<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Scott Brooks</title>
  <subtitle></subtitle>
  <link href="https://scottbrooks.info/feed.xml" rel="self"/>
  <link href="https://scottbrooks.info/"/>
  
    <updated>2027-02-13T00:00:00Z</updated>
  
  <id>https://scottbrooks.info</id>
  <author>
    <name>Scott Brooks</name>
    <email>scottb@tsbrooks.com</email>
  </author>
  
    
    <entry>
      <title>Co-Owner, Project Lead, Programmer - WBSolution</title>
      <link href="https://scottbrooks.info/posts/co-owner-project-lead-programmer-wbsolution/"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/co-owner-project-lead-programmer-wbsolution/</id>
      <content type="html">
        <![CDATA[
      <p><strong>Mar 1999 - May 2003</strong></p>
<ul style="color:#000000">
	<li>GoPromo Website (ASP) - Dynamic public website.  Allows users to search and view Homes/Apartments/Mobiles in West Michigan.</li>
	<li>WBSolution Intranet (ASP) - Site developed to track project information.  I also wrote a VB6 application to export the data into a .QIF file that was imported into QuickBooks for billing.</li>
	<li>Location Monitor (VB6/ASP) - Web site providing information from our servers located in hospitals around the country.</li>
	<li>Marathon Specialty Products - Rocketport 16-port serial (VB6) ) - This application interfaces to a bank of 12+ serial printers and based on the part scanned, sends the correct label(s) to the correct printer(s)</li>
	<li>Health Level Seven Interface - HL7 (VB6) - IP Server application to collect hospital ADT (Admit/Discharge/Transfer) information.  This is a sold program running in more than 30 hospitals collecting data 24x7.</li>
	<li>Telephony Application (VB6) - Application plays interactive prompts for patients.  Based on patient input, the telephone and/or televison services are activated/deactivated (using interfaces discussed below).</li>
	<li>Interface, Siemens HiPath Telephone Switch - Serial (VB6) -</li>
	<li>Interface, NEC NEAX 2400/2400 Telephone Switch - Serial, API (VB6) -</li>
	<li>Interface, Alcatel Telephone Switch - Dialogic (VB6) -</li>
	<li>Interface, Nortel Option 81 Telephone Switch - Dialogic (VB6) -</li>
	<li>Interface, SL100 Telephone Switch - IP (VB6) -</li>
	<li>Interface, Lucent/Definity Telephone Switch - Dialogic (VB6) -</li>
	<li>eBay API Application (VB6/ASP)</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Director Software Support and Development - United Telemanagement Corp.</title>
      <link href="https://scottbrooks.info/posts/director-software-support-and-development-united-telemanagement-corp./"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/director-software-support-and-development-united-telemanagement-corp./</id>
      <content type="html">
        <![CDATA[
      <p><strong>May 2003 - Aug 2005</strong></p>
<ul style="color:#000000">
	<li>Hospital Software Installation and Upgrade - During this two year period, we installed and maintained our application in many hospitals. Most hospitals required a trip to install a server at the location, and another trip when the hospital went live on the system.</li>
	<li>Telelink Verison 3.0 (<b><i>ASP/VB6</i></b>) - I was one of three architects and the principle developer of the Next Generation software that runs the majority of their business.  Another developer and I had developed the original application in Foxpro DOS more than 8 years earlier. We moved the application into Active Server Pages, SQL Server 2K, and Visual Basic.</li>
	<li>Maintenance Programming (<b><i>VB6/ASP</i></b>) - Modified existing programs to provide additional functionality as business needs changed. I was the person primarily responsible for debugging and enhancing the applications developed by WBSolution.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Owner, Project Lead, Programmer - A7Dev</title>
      <link href="https://scottbrooks.info/posts/owner-project-lead-programmer-a7dev/"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/owner-project-lead-programmer-a7dev/</id>
      <content type="html">
        <![CDATA[
      <p><strong>Aug 2005 - Feb 2006</strong></p>
<ul style="color:#000000">
	<li>Kettering Seminars <b>Web Scheduler</b> Conversion <b><i>(ASP.Net/C#)</i></b> - This project involved converting an incomplete ASP application that had lingered for nearly two years.  By moving it to .NET/C#, I was able to complete the project.</li>
	<li>GoPromo WebBuilder Conversion (<b><i>ASP.NET/C#, SQL Server 2000</i></b>) - Upgrading this site to SQL2K (from Access) solved a major scalability problem.  Converting the code to C# provided a much richer and dynamic user interface.</li>
	<li>BradyWare Data Conversion, import, and folder maintenance programs (<i><b>Visual Basic 6</b></i>)</li>
	<li>Postal Zip code WebService (<b><i>C#</i></b>) - As a working demo for a customer, I built a web service that returns zip code, city, and state information.</li>
	<li>Yoder Industries <b><i>.NET Remoting</i></b>/serial interface to scale - The serial port portion of this app was done in C# 2.0.  It runs on a computer in the shipping area.  The computer from the data center uses .NET Remoting (1.1) to connect to the shipping computer and request the current weight from the scale.</li>
	<li>Network Administration - Responsible for every step of purchasing, building loading, and maintaining computers.  Maintain more than 150 sites in IIS (wrote program to automate new domains).  Maintain DNS servers (wrote another program to script adding new domains). Responsible for ISA Firewall, keeping computers patched, backed up, etc...</li>
	<li>Screen Scraping Application - <b><i>C#</i></b> windows application that gets the source of a web page every 15 seconds, and uses RegEx to parse the selected values into a database.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Programmer - Barcode Management Systems.</title>
      <link href="https://scottbrooks.info/posts/programmer-barcode-management-systems./"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/programmer-barcode-management-systems./</id>
      <content type="html">
        <![CDATA[
      <p><strong>Oct 1994 - May 1996</strong></p>
<ul style="color:#000000">
	<li><b>Summit Polymers</b>, Kalamazoo MI - Developed a Visual Basic Multidrop application that communicates with shop floor terminals to provide lot traceability for passenger airbag covers.</li>
	<li>Formerly <b>Zeneca AG Products</b>, North Little Rock AK - Wrote major portions of an application to print and apply barcode labels to jugs, boxes, and pallets.  Also developed a status screen that displayed real time production data.</li>
	<li><b>Navistar</b>, Springfield OH - Developed a client server ODBC (Watcom SQL) VB interface between an Intermec Janus 2020 scanner used for quality assurance.</li>
	<li><b>AAP</b>, St. Marys OH - Completed an application used for shop floor data collection system that uses ComputerWise terminals and Vertex Bridgenet software.  Used Foxpro for reporting.</li>
	<li><b>Team Building</b> - Implemented version control software (Sourcesafe), and trained staff on how to use it. Instituted coding standards and created a library of reusable components.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Programmer - Pillar Technology (Cintas, Mason OH)</title>
      <link href="https://scottbrooks.info/posts/programmer-pillar-technology-(cintas-mason-oh)/"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/programmer-pillar-technology-(cintas-mason-oh)/</id>
      <content type="html">
        <![CDATA[
      <p><strong>Mar 2006 - Jul 2007</strong></p>
<ul style="color:#000000">
	<li><b>ASP.Net development</b> - using C#, Visual Studio Team System Tests, Caliber, StarTeam</li>
	<li><b>WATIR UI Testing</b> - Modified Paraesthesia library to use VSTS tests (instead of NUnit), and provide a mechanism to be able to pass variables between tests. Wrote tests that perform all steps required to submit an order.</li>
	<li><b>Database Maintenance</b> - Responsible for keeping Oracle development database changes synchronized with QA database. Imported data from Legacy systems (SQL 2K) into Oracle.</li>
	<li><b>Continuous Integration Server</b> - (CruiseControl) Setup new projects and modified existing projects on the build server.</li>
	<li><b>Project Deployment</b> - Wrote detailed MSBuild script and batch file to checkout source from version control (StarTeam), build projects, and deploy to selected servers.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Programmer - Strategic Data Systems (Resurgent Capital Services)</title>
      <link href="https://scottbrooks.info/posts/programmer-strategic-data-systems-(resurgent-capital-services)/"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/programmer-strategic-data-systems-(resurgent-capital-services)/</id>
      <content type="html">
        <![CDATA[
      <p><strong>Jul 2007 - Jun 2008</strong></p>
<ul style="color:#000000">
	<li><span style="font-weight:bold">ASP.Net development</span> - using both C# and VB.Net. Worked with customer to complete final changes on application that had been in UAT for more than a year.</li>
	<li><span style="font-weight:bold">.Net C# development</span> - Wrote windows service that watches for files and transfers the files to customer FTP server. We used nLog to create detailed log messages that have assisted in making this a very robust application.</li>
	<li><span style="font-weight:bold">SQL Server Reports</span> - Created SQL Server reports for reporting metrics from application.</li>
	<li><span style="font-weight:bold">VB.Net development</span> - Wrote windows service that launches multiple instances of SQL Server stored procedures and specified intervals and monitors for deadlocks.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>IT Director, Senior Programmer - United Telemanagement Corp.</title>
      <link href="https://scottbrooks.info/posts/it-director-senior-programmer-united-telemanagement-corp./"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/it-director-senior-programmer-united-telemanagement-corp./</id>
      <content type="html">
        <![CDATA[
      <p><strong>Jul 2011 - Jun 2014</strong><span class="projectSubText">(Contractor from Feb-Jun 2014)</span></p>
<ul style="color:#000000">
	<li>Served as IT director, supervising both development and support teams.</li>
	<li><b>ASP.Net MVC, jQuery, Ajax</b> - Designed and assisted coding of a <b>single page application</b> to allow call center agents to accept payments for patients.</li>
	<li>Attended <b>Architecting with Amazon Web Services</b> training in preparation for moving our core business software to the cloud.</li>
</ul>
<strong>Jun 2008 - Jul 2011</strong>
<ul style="color:#000000">
<li><b><a href="/posts/hospital-update-program">Hospital Update Program</a></b> Created application to ensure each location is running latest version of software.</li>
	<li>Wrote interface calling <a href="http://developer.teamwork.com/" target="_blank" rel="noopener noreferrer">TeamworkPM</a> and publishing updates to <a href="https://www.hipchat.com/docs/apiv2" target="_blank" rel="noopener noreferrer">HipChat API</a> used in our call center.</li>
	<li><b>HL7 Library</b> (C#) - Wrote HL7 server application to accept messages from hospital, create c# objects for each message, and store required elements to database.</li>
<li>Created messaging service using Amazon Simple Queue Service (SQS) delivering more than 10,000 messages daily.</li>
	<li>Converted Visual Basic programs to C# Windows Services for 24x7 applications running at hospitals.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>System Analyst, Programmer - Sidney MicroSystems</title>
      <link href="https://scottbrooks.info/posts/system-analyst-programmer-sidney-microsystems/"/>
      <updated>2014-05-18T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/system-analyst-programmer-sidney-microsystems/</id>
      <content type="html">
        <![CDATA[
      <p><strong>May 1996 - Mar 1999</strong></p>
<ul style="color: #000000">
	<li><b>Developed CORE</b> - Developed a "Compoment Oriented Reusable Environment" system in Visual Basic that allows transparent communications between various shop floor terminals (Intermec, Computerwise, and a few others).  This system handled the basic communications plumbing with the devices.  This allowed the other developers to focus on the specific business requirements.</li>
	<li><b>Hartzell Fan</b> - Wrote the pricing module of a sales order application.  This application was written in Visual Basic 4-16 so that it could run on Windows 3.1 (Many of the sales reps had older computers)</li>
	<li><b>Handheld Serial Transfer</b> - Wrote the PC side (in Visual Basic) that transfers information to and from an Intermec Janus handheld.</li>
	<li><b>Wally Byam Caravan Club</b> - Converted the membership system from dBase III+ to Foxpro for Windows (version 2.6)</li>
	<li><b>Access DB/Excel Automation</b> - Wrote a Visual Basic app that allows the user to enter equipment downtime information into an Access database.  Also wrote Visual Basic for Applications code in Excel to pull the data from the Access database.</li>
	<li><b>Sales Order System</b> - Converted to a multi-user MDI based Visual Basic Application using Microsoft SQL Server as the database engine.</li>
	<li><b>Goodyear</b> - Developed a Visual Basic touch screen application used on a shop floor that allows operators to easily select the next job to start and view information about the current job.</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Using Wordpress to display my Resume</title>
      <link href="https://scottbrooks.info/posts/using-wordpress-to-display-my-resume/"/>
      <updated>2014-05-19T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/using-wordpress-to-display-my-resume/</id>
      <content type="html">
        <![CDATA[
      <p>I have used WordPress in the past for Luke's website (<a href="http://www.growingupluke.com" target="_blank" rel="noopener noreferrer">GrowingUpLuke</a>) and I found it very easy to get a decent looking site up quickly. It allowed me to focus on the content, while it handled all the plumbing of displaying it.</p>
<p>However, when I considered using WordPress for my resume, I could not find a way to create a custom page with only specific posts on that page.  I wanted to create an Experience page (<a href="http://scottbrooks.info/experience/" target="_blank" rel="noopener noreferrer">like this</a>) that showed only my job history, but not other posts.</p>
<p>I found a plugin that would allow me to do it, but when I looked for the Plugin menu item, I could not find it.  After a little searching I found this text, &quot;<a href="http://en.support.wordpress.com/plugins/" target="_blank" rel="noopener noreferrer">...WordPress.com users won’t find a Plugins tab in the Dashboard</a>&quot; on the WordPress.com site.</p>
<p>I considered hosting the WordPress site on my existing server, but it seemed much easier (and fairly cheap) to sign up with <a href="http://www.bluehost.com/cgi/info/wordpress" target="_blank" rel="noopener noreferrer">Bluehost</a>, since they were recommended from the <a href="http://wordpress.org/hosting/" target="_blank" rel="noopener noreferrer">WordPress.org</a> site.</p>
<p>Here is the list of Plugins I have installed so far:</p>
<ul>
	<li><a href="https://wordpress.org/plugins/add-posts-to-pages/" target="_blank" rel="noopener noreferrer">Add Posts to Pages</a> - Makes it easy to create a page of specific posts.</li>
	<li><a href="http://dublue.com/plugins/toc/" target="_blank" rel="noopener noreferrer">Table of Contents Plus</a> - Simple way to insert a table of contents into a post.</li>
	<li><a href="https://wordpress.org/plugins/simple-custom-css/" target="_blank" rel="noopener noreferrer">Simple Custom CSS</a> - Allow custom CSS.
<span style="font-size: 10px;color: #808080"><em>(I needed to add some space after each list item on my <a href="http://scottbrooks.info/about-scott-brooks/" target="_blank" rel="noopener noreferrer">about page</a>. This Plugin allowed me to accomplish this
by adding this CSS: "article ol li { padding-bottom: 15px; }")</em></span>
<span style="font-size: 10px;font-weight: bold">Update: While creating this page, I discovered I could have just used a &lt;div&gt;. Oh well, I know for next time. :-)</span></li>
	<li><a href="https://github.com/bradyvercher/gistpress" target="_blank" rel="noopener noreferrer">GistPress</a> - A WordPress plugin to easily embed Gists via oEmbed or shortcode.</li>
	<li><a href="http://wordpress.org/plugins/syntaxhighlighter/" target="_blank" rel="noopener noreferrer">SyntaxHighlighter Evolved</a> - easily post syntax-highlighted code to your site (<a href="http://alexgorbatchev.com/SyntaxHighlighter/manual/brushes/" target="_blank" rel="noopener noreferrer">brushes</a>)</li>
	<li><a href="http://wordpress.org/plugins/printfriendly/" target="_blank" rel="noopener noreferrer">Print Friendly and PDF Button</a> - Printer Friendly pages (<a href="http://support.printfriendly.com/" target="_blank" rel="noopener noreferrer">Support</a>)</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Triage - Amazon Marketplace Web Services - Product Finder</title>
      <link href="https://scottbrooks.info/posts/triage-amazon-marketplace-web-services-product-finder/"/>
      <updated>2014-05-19T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/triage-amazon-marketplace-web-services-product-finder/</id>
      <content type="html">
        <![CDATA[
      <h1>Summary</h1>
The purpose of this project was to provide an easy way to determine if a book (or other media) is sellable on Amazon. It uses the MWS (Marketplace Web Services) API provided by Amazon that allows merchants to programmatically send and receive information.
<p>A major consideration for this project was to not &quot;waste&quot; API calls. Amazon, like most APIs, limits the number of requests that can be submitted in a given amount of time (<a href="#Throttling">Throttling</a>).</p>
<p>When searching for a product on Amazon, they often return multiple items for a given query. Most of them are not a match, and need to be eliminated from consideration. Once the user has determined which products are a match, they click the Get button. An Ajax call is made to our server, which makes the API call to Amazon. This is getting a bit wordy, but the screens below should help make sense of it.</p>
<h1>Description</h1>
I named this project the Triage project since it reminds me of the television series <a title="M*A*S*H" href="http://en.wikipedia.org/wiki/M*A*S*H_(TV_series)" target="_blank" rel="noopener noreferrer">M*A*S*H</a> where the doctors look over the new arrivals and decide which patients need immediate attention. Some are prepped for surgery while others are moved directly to post-op.  That is similar to how we need to handle the batches of books.  For ones that appear battered or not in very good shape, they will go in one pile.  Ones that are missing a barcode will go in a different pile, and on and on.
<p>When we have a good pile of products to investigate, we need so scan each one and determine if it is worth sending to Amazon.  That is the purpose of this program.</p>
<h2>Design</h2>
I broke this project into the following layers.
<h3>Data Layer</h3>
There are two primary sources of data for this application.
<h4>MySql / SQLite</h4>
The project has a pretty simple database for the information about each book. Some basic information like the source of the book, weight, condition, price and a few other things are stored in the database. This information is used when the listing is submitted to Amazon. I am using <a href="http://nhforge.org/" target="_blank" rel="noopener noreferrer">NHibernate</a> as the ORM for the data layer. This makes it very easy to work with <a href="http://www.sqlite.org/" target="_blank" rel="noopener noreferrer">SQLite</a> for development, and then change to MySql for production with a simple web.config change.
<p>I also used <a href="http://www.fluentnhibernate.org/" target="_blank" rel="noopener noreferrer">Fluent NHibernate</a> to make configuration simple. I especially like the Schema generation feature where I can just add a property to an object during development, and have my database generated from the objects. This along with the SQLite makes the development side much more efficient.</p>
<h4>Amazon Marketplace Web Services</h4>
The Amazon MWS API consists of multiple sections. Most of what is required for this app is in the <a href="http://docs.developer.amazonservices.com/en_US/products/index.html" target="_blank" rel="noopener noreferrer">Products</a> section of the API.
Methods called include:
<ul>
	<li>
<h5>ListMatchingProducts</h5>
</li>
	<li>
<h5>GetCompetitivePricingForASIN</h5>
</li>
	<li>
<h5>GetLowestOfferListingsForASIN</h5>
</li>
</ul>
<h3>Business Layer (with Entities)</h3>
The project uses a manager class that calls the MWS API and converts the objects into our entity objects.
For a bigger project, I would have added a Service Layer that would have controlled access to the Business layer and provided a REST interface to the client applications.  However, for this project, the MVC Controllers call the Business layer directly.
<h3>Presentation Layer (Asp.net MVC)</h3>
We are not shooting for any awards here.  This project has one of the most plain looks of anything I have done recently.  My goal was to keep it as uncluttered and easy to use as possible.
<h2>Screens</h2>
In the <a title="Stephen Covey" href="http://en.wikipedia.org/wiki/The_Seven_Habits_of_Highly_Effective_People" target="_blank" rel="noopener noreferrer">Stephen Covey</a> fashion of "Begin with the End in Mind", here are some screenshots of what I was shooting for.
<h3>Input Form</h3>
<a href="/images/uploads/sites/2/2015/05/TriageInput.png" target="_blank" rel="noopener noreferrer"><img class="alignnone size-large wp-image-161" src="/images/uploads/sites/2/2015/05/TriageInput-1024x278.png" alt="Triage Input" height="169" /></a>
<p>This is a pretty simple screen. It allows the user to scan a barcode or enter text for a lookup. When scanning a barcode, the scanner adds an &lt;ENTER&gt; after the barcode. The following code was added so the lookup button will be clicked &quot;automatically&quot; after a scan.</p>
<p>[code language=&quot;Javascript&quot;]
$(&quot;#txtLookup&quot;).keyup(function (event) {
// console.log(&quot;lookup keypress: %O&quot;, event);
var KEYCODE_ENTER = 13;
if (event.keyCode == KEYCODE_ENTER) {
$(&quot;#btnLookup&quot;).click();
}
});
[/code]</p>
<h3 class="page-break-before">List View</h3>
I make one API call to get the results for the input. Often, items will be returned which do not match the product we are looking for. This screen allows us to eliminate the ones we don't care about.  The user will click the Hide button, and the item will be removed.
<a href="/images/uploads/2014/05/TriageList.png" target="_blank" rel="noopener noreferrer"><img class="alignnone size-large wp-image-156" src="/images/uploads/2014/05/TriageList-1024x754.png" alt="TriageList" height="460" /></a>
<h3>Results View</h3>
Finally, we get the actual results we are looking for.
<a href="/images/uploads/2014/05/TriageResult.png" target="blank" rel="noopener noreferrer"><img class="alignnone size-large wp-image-157" src="/images/uploads/2014/05/TriageResult-1024x751.png" alt="TriageResult" height="458" /></a>
<h2>Considerations</h2>
<h3>Quick and easy (for the user)</h3>
I wanted it to be very easy for a new user to start being productive with this screen.  All they need is a browser and a pile of books and they can start scanning.
<h3>Go/NoGo</h3>
The original goal was to have a simple Go/NoGo (Red/Green) indicator on the screen to let the user know if we wanted to send the book to Amazon.  That will involve a good deal more analysis  of the many variables that play into that decision.   For a first pass, I may do a Red/Yellow/Green indicator.  I could inform the books we DO want to send (Green) and the ones we do NOT want to send (Red), and make everything else Yellow so they can determine if we should send it.
<h3>Throttling</h3>
<a href="http://docs.developer.amazonservices.com/en_US/products/Products_Throttling.html" target="_blank" rel="noopener noreferrer">Throttling</a> must be considered when developing APIs. (That is a big part of the reason this project was an Asp.Net MVC application using Ajax). When we lookup a product on Amazon, we often get more than one result. Most of the results do not match the product we are searching, so we don't want to "waste" our API calls on products we don't care about.
<h2>Code Samples/Examples</h2>
<h3>Javascript ReplaceAll method</h3>
The Javascript <a title="Replace" href="http://www.w3schools.com/jsref/jsref_replace.asp" target="_blank" rel="noopener noreferrer">Replace</a> method only replaces the first occurrence of a string. This function will replaces all occurrences of a string.  It is just a one liner and I didn't write it, but I wanted to include it for future reference.
[gist id="a7aff4b98a247eecff53"]
<div class="viewRaw">Click the "view raw" link to view the entire code listing.</div>
<a href="#">top</a>
<h3>UrlEncodeObj</h3>
Return object as URL encoded string. I didn't write this function (I hate one character variable names). I think it was provided by a fellow we call "Little Ben Hooper".
[gist id="0d628321cf2b50da4cc9"]
<div class="viewRaw">Click the "view raw" link to view the entire code listing.</div>
<a href="#">top</a>
<h3>ViewModel</h3>
I like to use the view model for the page. This makes it much easier to pass all required objects to a view. It also makes it much easier to add future properties and objects.
<p>[code language=&quot;CSharp&quot;]
public class ProductViewModel
{
public string Filter { get; set; }
public string Condition { get; set; }
private List&lt;SelectListItem&gt; _conditionList;</p>
<p>public ProductViewModel()
{
_conditionList = new List&lt;SelectListItem&gt;();
_conditionList.Add(new SelectListItem { Text = &quot;New&quot;, Value = &quot;New&quot; });
_conditionList.Add(new SelectListItem { Text = &quot;Used, Like New&quot;, Value = &quot;UsedLikeNew&quot; });
_conditionList.Add(new SelectListItem { Text = &quot;Used, Very Good&quot;, Value = &quot;UsedVeryGood&quot; });
_conditionList.Add(new SelectListItem { Text = &quot;Used, Good&quot;, Value = &quot;UsedGood&quot; });
_conditionList.Add(new SelectListItem { Text = &quot;Used, Acceptable&quot;, Value = &quot;UsedAcceptable&quot; });
}</p>
<p>public IEnumerable&lt;SelectListItem&gt; ConditionList
{
get { return _conditionList.AsEnumerable(); }
}
}
[/code]</p>
<h3>Page Markup</h3>
This makes the actual page markup pretty simple.
<p>[code language=&quot;html&quot; highlight=&quot;1&quot;]
@model Bfl.TriageWeb.ProductViewModel
@{
ViewBag.Title = &quot;Index&quot;;
}
&lt;h3&gt;MatchingProducts&lt;/h3&gt;
&lt;button id=&quot;btnClear&quot; title=&quot;Clear Input field&quot;&gt;X&lt;/button&gt;
&lt;input type=&quot;text&quot; id=&quot;txtLookup&quot; value=&quot;&quot; autofocus /&gt;
&lt;button id=&quot;btnLookup&quot;&gt;Lookup&lt;/button&gt;
&lt;br /&gt;
Last Input: &lt;b&gt;&lt;span id=&quot;lastInput&quot;&gt;&lt;/span&gt;&lt;/b&gt;</p>
<p>&lt;div&gt;
Filter:
@Html.RadioButtonFor(x =&gt; x.Filter, &quot;&quot;, new { @class = &quot;radioButton&quot; })None
@Html.RadioButtonFor(x =&gt; x.Filter, &quot;VHS&quot;, new { @class = &quot;radioButton&quot; })VHS
@Html.RadioButtonFor(x =&gt; x.Filter, &quot;DVD&quot;, new { @class = &quot;radioButton&quot; })DVD
@Html.RadioButtonFor(x =&gt; x.Filter, &quot;Books&quot;, new { @class = &quot;radioButton&quot; })Books
&lt;/div&gt;
&lt;table id=&quot;tblProduct&quot;&gt;
&lt;tbody valign=&quot;top&quot;&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;hr /&gt;
[/code]</p>
<h3>Ajax Request</h3>
When the user clicks the Get button, the following method is called.  It uses the index of the Get button clicked to change the caption and disable the button.
[code language="csharp"]
function ButtonGet(index, key) {
  // console.log(&quot;ButtonGet index: %O, key: %O&quot;, index, key);
  var data = {};
  data.index = index;
  data.asin = key;
<p>// Disable the button and change the caption.
$('#btn' + index).html(&quot;Retrieving Item...&quot;);
$('#btn' + index).attr(&quot;disabled&quot;, &quot;disabled&quot;);</p>
<p>$.ajax(&quot;/Product/GetLowestOffer/&quot;, {
type: &quot;POST&quot;,
dataType: 'json',
data: UrlEncodeObj(data),
success: ShowOneItem
});
}
[/code]</p>
<p>The controller returns a ViewModel for this specific request.
[code language=&quot;csharp&quot;]
public class OfferInfoViewModel
{
public string Index { get; set; }
public string Asin { get; set; }
public List&lt;LowestOfferDto&gt; AmazonOffers { get; set; }
public List&lt;LowestOfferDto&gt; MerchantOffers { get; set; }</p>
<p>public CompetitivePricingDto CompetitivePricing { get; set; }</p>
<p>public OfferInfoViewModel()
{
AmazonOffers = new List&lt;LowestOfferDto&gt;();
MerchantOffers = new List&lt;LowestOfferDto&gt;();
}
}
[/code]</p>
<p>And here is the controller code for the Ajax call.
[code language=&quot;csharp&quot;]
public string GetLowestOffer()
{
string logMsg = LOG_PREFIX + &quot;GetLowestOffer&quot;;</p>
<p>OfferInfoViewModel retVal = new OfferInfoViewModel();
try
{
if (Request.Form.Count &gt; 0)
{
string asin = Request.Form[&quot;asin&quot;];
string index = Request.Form[&quot;index&quot;];</p>
<p>AmazonMwsProductManager mgr = new AmazonMwsProductManager();</p>
<p>LowestOfferListingsListResult lowestList = mgr.GetLowestOfferListingsForAsin(asin);
retVal.Asin = asin;
retVal.Index = index;</p>
<p>Logging.LogTrace(&quot;Lowest Offer Result for Asin: &quot; + asin);
StringBuilder sb = new StringBuilder();
foreach (LowestOfferListingsResult lowestOffer in lowestList)
{
LowestOfferDto oneItem = new LowestOfferDto(lowestOffer);</p>
<p>if (lowestOffer.FulfillmentChannel.ToLower().Contains(&quot;amazon&quot;))
{ retVal.AmazonOffers.Add(oneItem); }
else
{ retVal.MerchantOffers.Add(oneItem); }
}</p>
<p>CompetitivePricingResult competitivePricingResult =  mgr.GetCompetitivePricingForAsin(asin);</p>
<p>retVal.CompetitivePricing = new CompetitivePricingDto(competitivePricingResult);</p>
<p>ProductCategoryListResult categoryList = mgr.GetProductCategoriesForAsin(asin);
retVal.CompetitivePricing.AddCategoryNameForSalesRank(categoryList);
}
}
catch (Exception ex)
{ Logging.LogError(logMsg + &quot;, EXCEPTION: &quot; + ex.Message, ex); }</p>
<p>return JsonConvert.SerializeObject(retVal);
}
[/code]</p>
<h3>Ajax Response</h3>
<p>This code needs work.</p>
<ul>
<li>The inline styles need to be removed and placed into the site.css.</li>
<li>It needs to be converted to use something like <a href="http://www.jsviews.com/#jsrapi" target="_blank" rel="noopener noreferrer">JsRender</a></li>
<li>Portions need to be extracted into separate functions</li>
</ul>
Even with those disclaimers, I was still hesitant to post this code.  In my experience, writing code is a very iterative process.  We don't always get what we want the first time.  Often, we "try" something (like an inline style) to see if we get what we are looking for.  Good code is a process of continuous refactoring.
In some ways, this snippet is "mid-refactor".  I should clean it up before posting it, but I plan to make the JsView refactor a separate post later.
<p>[code language=&quot;javascript&quot;]
function ShowOneItem(data) {
console.log(&quot;ShowOneItem, data: %O&quot;, data);</p>
<p>$('#btn' + data.Index).html(&quot;Get&quot;);
$('#btn' + data.Index).removeAttr(&quot;disabled&quot;);</p>
<p>var rowFba = &quot;&quot;;</p>
<p>$.each(data.AmazonOffers, function (index, item) {
rowFba += &quot;&quot; +
&quot;&lt;tr width='400px' &gt;&quot; +
&quot;&lt;td nowrap&gt;&quot; + item.LandedPrice +
&quot;&lt;span style='font-size: small;'&gt; (&quot; + item.ListedPrice + &quot; + &quot; + item.Shipping + &quot;) &lt;/span&gt;&lt;/td&gt;&quot; +
&quot;&lt;td nowrap&gt;&quot; + item.Condition + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td align='right'&gt;&quot; + item.NumberOfOfferListingsConsidered + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td align='right' nowrap&gt;&quot; + item.SellerPositiveFeedbackRating + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td align='right'&gt;&quot; + item.SellerFeedbackCount + &quot;&lt;/td&gt;&quot; +
&quot;&lt;/tr&gt;&quot;;
});</p>
<p>var rowMerchant = &quot;&quot;;</p>
<p>$.each(data.MerchantOffers, function (index, item) {
rowMerchant += &quot;&quot; +
&quot;&lt;tr&gt;&quot; +
&quot;&lt;td nowrap&gt;&quot; + item.LandedPrice +
&quot;&lt;span style='font-size: small;'&gt; (&quot; + item.ListedPrice + &quot; + &quot; + item.Shipping + &quot;) &lt;/span&gt;&lt;/td&gt;&quot; +
&quot;&lt;td nowrap&gt;&quot; + item.Condition + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td align='right'&gt;&quot; + item.NumberOfOfferListingsConsidered + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td align='right' nowrap&gt;&quot; + item.SellerPositiveFeedbackRating + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td align='right'&gt;&quot; + item.SellerFeedbackCount + &quot;&lt;/td&gt;&quot; +
&quot;&lt;/tr&gt;&quot;;
});</p>
<p>var rowSalesRank = &quot;&quot;;
$.each(data.CompetitivePricing.SalesRankings, function (index, item) {
rowSalesRank += &quot;&quot; +
&quot;&lt;tr style='background-color: #A2D39C;'&gt;&quot; +
&quot;&lt;td align='right'&gt;&quot; + item.Rank + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&quot; + item.CategoryName + &quot;  (&quot; + item.ProductCategoryId + &quot;)&lt;/td&gt;&quot; +
&quot;&lt;/tr&gt;&quot;;
});</p>
<p>// Build the Html for the item.
var html = &quot;&quot; +
&quot;&lt;table class='itemTable' &gt;&quot; +
&quot;&lt;tr&gt;&quot; +
&quot;&lt;th&gt;&amp;nbsp;&lt;/th&gt;&quot; +
&quot;&lt;th&gt;&amp;nbsp;&lt;/th&gt;&quot; +
&quot;&lt;th&gt;&amp;nbsp;&lt;/th&gt;&quot; +
&quot;&lt;th colspan='2' nowrap &gt;----Feedback----&lt;/th&gt;&quot; +
&quot;&lt;/tr&gt;&quot; +
&quot;&lt;tr style='border-bottom: solid medium blue;'&gt;&quot; +
&quot;&lt;th nowrap&gt;Price &lt;span style='font-size: 10px;' &gt;(Listed + Shipping)&lt;/span&gt;&lt;/th&gt;&quot; +
&quot;&lt;th&gt;Condition&lt;/th&gt;&quot; +
&quot;&lt;th align='right'&gt;Offers&lt;/th&gt;&quot; +
&quot;&lt;th align='right'&gt;Rating&lt;/th&gt;&quot; +
&quot;&lt;th align='right'&gt;Count&lt;/th&gt;&quot; +
&quot;&lt;/tr&gt;&quot; +
&quot;&lt;tbody&gt;&quot; +
&quot;&lt;tr&gt;&lt;td colspan='9'&gt;&lt;h3&gt;Lowest FBA Offers&lt;/h3&gt;&lt;/td&gt;&lt;/tr&gt;&quot; +
rowFba +
&quot;&lt;tr&gt;&lt;td colspan='9'&gt;&lt;h3&gt;Lowest Merchant Offers&lt;/h3&gt;&lt;/td&gt;&lt;/tr&gt;&quot; +
rowMerchant;</p>
<p>html = html +
&quot;&lt;tr&gt;&lt;td colspan='9'&gt;&lt;h3&gt;Buy Box Price&lt;/h3&gt;&lt;/td&gt;&lt;/tr&gt;&quot;;
if (data.CompetitivePricing.NewPrice.Condition != null) {
html = html + &quot;&quot; +
&quot;&lt;tr style='background-color: #FDC68A;'&gt;&quot; +
&quot;&lt;td nowrap&gt;&quot; + data.CompetitivePricing.NewPrice.LandedPrice +
&quot;&lt;span style='font-size: small;'&gt; (&quot; + data.CompetitivePricing.NewPrice.ListedPrice + &quot; + &quot; +
data.CompetitivePricing.NewPrice.Shipping + &quot;) &lt;/span&gt;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&quot; + data.CompetitivePricing.NewPrice.DisplayCondition + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&quot; +
&quot;&lt;/tr&gt;&quot;;
}
if (data.CompetitivePricing.UsedPrice.Condition != null) {
html = html + &quot;&quot; +
&quot;&lt;tr&gt;&quot; +
&quot;&lt;td nowrap&gt;&quot; + data.CompetitivePricing.UsedPrice.LandedPrice +
&quot;&lt;span style='font-size: small;'&gt; (&quot; + data.CompetitivePricing.UsedPrice.ListedPrice + &quot; + &quot; +
data.CompetitivePricing.UsedPrice.Shipping + &quot;) &lt;/span&gt;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&quot; + data.CompetitivePricing.UsedPrice.DisplayCondition + &quot;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&quot; +
&quot;&lt;td&gt;&amp;nbsp;&lt;/td&gt;&quot; +
&quot;&lt;/tr&gt;&quot;;
}</p>
<p>html = html +
&quot;&lt;/tbody&gt;&quot; +
&quot;&lt;/table&gt;&quot;;</p>
<p>// Add the Sales Ranking
html = html +
&quot;&lt;table class='itemTable'&gt;&quot; +
&quot;&lt;tr style='border-bottom: solid medium blue;'&gt;&quot; +
&quot;&lt;th align='right'&gt;Rank&lt;/th&gt;&quot; +
&quot;&lt;th align='right'&gt;Category&lt;/th&gt;&quot; +
&quot;&lt;/tr&gt;&quot; +
&quot;&lt;tbody&gt;&quot; +
rowSalesRank +
&quot;&lt;/tbody&gt;&quot; +
&quot;&lt;/table&gt;&quot;;
html = html +
&quot;&lt;br /&gt;&lt;div style='font-size: Large;'&gt;&quot; +
&quot;&lt;b&gt;Listings Available: &lt;/b&gt;&quot;;</p>
<p>if (data.CompetitivePricing.NumberOfListingsNew != &quot;0&quot;) {
html = html +
&quot;&lt;a href='http://www.amazon.com/gp/offer-listing/&quot; + data.Asin +
&quot;/ref=dp_olp_new?ie=UTF8&amp;condition=new' &quot; +
&quot; target='_blank' &gt;&quot; +
data.CompetitivePricing.NumberOfListingsNew + &quot; new&quot; +
&quot;&lt;/a&gt; &quot;;
}</p>
<p>if (data.CompetitivePricing.NumberOfListingsUsed != &quot;0&quot;) {
html = html +
&quot;&lt;a href='http://www.amazon.com/gp/offer-listing/&quot; + data.Asin +
&quot;/ref=dp_olp_used?ie=UTF8&amp;condition=used' &quot; +
&quot; target='_blank' &gt;&quot; +
data.CompetitivePricing.NumberOfListingsUsed + &quot; used&quot; +
&quot;&lt;/a&gt; &quot;;
}</p>
<p>if (data.CompetitivePricing.NumberOfListingsCollectible != &quot;0&quot;) {
html = html +
&quot;&lt;a href='http://www.amazon.com/gp/offer-listing/&quot; + data.Asin +
&quot;/ref=dp_olp_collectible?ie=UTF8&amp;condition=collectible' &quot; +
&quot; target='_blank' &gt;&quot; +
data.CompetitivePricing.NumberOfListingsCollectible + &quot; collectible&quot; +
&quot;&lt;/a&gt; &quot;;
}
html = html + &quot;&lt;/div&gt;&quot;;</p>
<p>$(&quot;#divLow&quot; + data.Index).html(html);
}
[/code]</p>
<h1>Technology</h1>
<ul>
	<li>Asp.Net MVC 4.5</li>
	<li>C#</li>
	<li>Amazon Marketplace Web Services</li>
	<li>jQuery</li>
	<li>Ajax</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Hospital Update Program</title>
      <link href="https://scottbrooks.info/posts/hospital-update-program/"/>
      <updated>2014-05-20T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/hospital-update-program/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
We install and maintain servers at customer locations around the country. Often, these servers are sitting in dark closets in basements without any human interaction. They are required to run 24x7 with a small window of time in the <a href="http://en.wiktionary.org/wiki/wee_hours" target="_blank" rel="noopener noreferrer">"wee hours"</a> of the night for updates.
Our software runs as Windows Services and we have multiple services on each server. If our software is interfacing with a specific piece of hardware using a serial connection, that is one service. If we are listening for a call on a Dialogic Telephony card, that is a different service. Before this project was implemented, it was becoming very difficult to keep each location running the correct version of the software.
<h2>Description</h2>
This project consists of three major components.  First, it has a Website component for administrators to view and edit information.  Second, it has a console program that checks for and actually performs the updates.  Finally, it has web services that expose functionality that both the Website and Console program need.
<p>The original version was deployed on Microsoft Azure using Sql Azure as the database.  It is currently being moved to Amazon servers using a SQL Server deployed on Amazon Web Services.  The actual files were saved to Azure using a CloudStorageAccount, but are now being saved to Amazon S3 storage.</p>
<p>A WatchDog service at each location runs the Hup.exe console program hourly.  Hup compares the current running version of each service to the required version for that location.  When it finds a difference, it downloads the new version into a new folder.  After it successfully downloads the files, it stops and unregisters the old version of the windows service.  It then fires up the new version.</p>
<p>Here is an example of how a folder looks with multiple versions.
<a href="/images/uploads/sites/2/2015/05/HupFolder.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/sites/2/2015/05/HupFolder.png" alt="HupFolder" class="alignnone size-full wp-image-432" /></a></p>
<h3>Website (Asp.Net MVC3, ASPX Views)</h3>
I am not a designer (seems like I say that in every post). I always liked the clean simple look of Google, and Gmail was no exception. Since this was an intranet site, I figured Google would not mind (or find out) that I "borrowed" their look.
<p>This project started as an MVC2 project, so the Razor view engine was not a possibility.  When I upgraded it to MVC3, it didn't make sense to refactor the views.  I have used Razor on subsequent projects, and I definitely prefer it to the ASPX view engine.</p>
<h4>List Screen</h4>
Here is an example List screen.  It includes paging information with the total record count and the current "page" being viewed.  Clicking the "Show Filter" link brings up the next screen.
<a href="/images/uploads/2015/05/HupAppList.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/2015/05/HupAppList-1024x770.png" alt="HupAppList" height="469" class="alignnone size-large wp-image-389" /></a>
<a href="#">top</a>
<h4 class="page-break-before">Filter Screen</h4>
The user can filter the results based on predetermined values.  This specific screen only allows the user to filter based on the Application.  They can also select the number of records they want to view per page.  Finally, they can select from a predefined list of Sort fields to order the results.
<a href="/images/uploads/2015/05/HupAppFilter.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/2015/05/HupAppFilter-1024x772.png" alt="HupAppFilter" height="471" class="alignnone size-large wp-image-388" /></a>
<a href="#">top</a>
<h4 class="page-break-before">Edit Screen</h4>
Here is an example Edit screen.  The preferred action is to click the Save button, but if they want to cancel their changes, they can click the "Back to List" link to cancel their changes.
<a href="/images/uploads/2015/05/HupAppEdit.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/2015/05/HupAppEdit-1024x773.png" alt="HupAppEdit" height="471" class="alignnone size-large wp-image-386" /></a>
<a href="#">top</a>
<h4 class="page-break-before">Set Current Version</h4>
This is one of the more critical screens in the application.  Here, the user can chose what applications run for each specific location.  Our server may be talking to one piece of hardware at one location, and completely different type of hardware at another location.  In addition to the application, the user an also select what version needs to be ran at that location.  Usually, we want to run the latest version, but not always.  This screen gives us that flexibility.
<a href="/images/uploads/2015/05/HupAssign.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/2015/05/HupAssign-1024x774.png" alt="HupAssign" height="472" class="alignnone size-large wp-image-393" /></a>
<a href="#">top</a>
<p>Some services run multiple copies of the same service.  For a Dialogic Telephony card listening on four lines, four separate services will run.  The Service Count column allows the user to specify how many should run.  In this example, the column is grayed out for the currently selected services, since each of them are only allowed to run one instance.</p>
<h4 class="page-break-before">Version Report</h4>
This was a very helpful report/screen that allows the user to know what version is running at each location.  This should be one of the first screens a tech should use when running down a problem at a location.  It has been especially helpful when a location is have a problem that we "thought we already fixed", only to find that a specific location is still running an older version of the code.
<a href="/images/uploads/2015/05/HupReport.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/2015/05/HupReport-1024x773.png" alt="HupReport" height="471" class="alignnone size-large wp-image-385" /></a>
<a href="#">top</a>
<h3 class="page-break-before">WCF Services</h3>
<h4>Common Methods</h4>
I am sure I stole this idea from somebody, but it has been so helpful to always include a few common methods in all web services I write.  It makes it nice to verify that the basics are working before I try to run down why some method that requires a complex object is not working.
<ul>
<li>Version - This is helpful for making sure the latest code actually got deployed.</li>
<li>ServerTime - Useful to make sure I am not fighting a cache issue and the result is changing each time I call the service.</li>
<li>Ping - Returns an Http 200 response status code, but no text.  When using a Load Balancer, I have it call this method to make sure the server is ready for requests.</li>
<p><a href="/images/uploads/2015/05/HupSnippet.png" target="_blank" rel="noopener noreferrer"><img src="/images/uploads/2015/05/HupSnippet-1024x1011.png" alt="HupSnippet" height="617" class="alignnone size-large wp-image-428" /></a>
<a href="#">top</a></p>
<h3>Console Program</h3>
Yes, an actual console program. This needed to be a simple .EXE that could be placed on a remote server. It only requires the .Net Framework and doesn't depend on any custom .DLLs. When it is ran without any parameters, it shows the following help screen.
<a href="/images/uploads/sites/2/2015/05/HupConsole.png" target="_blank" rel="noopener noreferrer"><img class="alignnone size-full wp-image-392" src="/images/uploads/sites/2/2015/05/HupConsole.png" alt="HupConsole" height="419" /></a>
<a href="#">top</a>
<p>It started with just a few simple switches, but grew to include other functions.</p>
<h2>Technology</h2>
<ul>
	<li>Service-Oriented Architecture</li>
	<li>Windows Communication Foundation (WCF)</li>
<li>Deployed on Microsoft Azure</li>
<li>Sql Azure Database</li>
	<li>Asp.Net MVC3 (not Razor)</li>
<li>Windows Service, Console Program</li>
</ul>
<a href="#">top</a>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>TeamworkPM REST Interface</title>
      <link href="https://scottbrooks.info/posts/teamworkpm-rest-interface/"/>
      <updated>2014-05-20T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/teamworkpm-rest-interface/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
Summary here...
<h2>Description</h2>
Detailed Description...
<h2>Technology</h2>
<ul>
	<li>C#</li>
	<li><a title="TeamworkPM API" href="http://developer.teamwork.com/" target="_blank" rel="noopener noreferrer">TeamworkPM API</a></li>
	<li>REST Web Services</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Move mission critical services to Amazon Web Services</title>
      <link href="https://scottbrooks.info/posts/move-mission-critical-services-to-amazon-web-services/"/>
      <updated>2014-05-21T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/move-mission-critical-services-to-amazon-web-services/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
Summary here...
<h2>Description</h2>
Detailed Description...
<h3>Amazon EC2 Metadata</h3>
We use the following class to obtain Metadata from a running Amazon instance. This code also demonstrates a way set a timeout variable when using the WebClient object.
<p>[gist id=&quot;c0417d583e53be09e3a3&quot; lines=&quot;50-54&quot;]<div class="viewRaw">Click the &quot;view raw&quot; link to view the entire code listing.</div></p>
<h2>Technology</h2>
<ul>
	<li>C#</li>
	<li>WebClient (with timeout)</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>BooksForLuke - Web Site</title>
      <link href="https://scottbrooks.info/posts/booksforluke-web-site/"/>
      <updated>2014-05-24T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/booksforluke-web-site/</id>
      <content type="html">
        <![CDATA[
      <p><a href="/images/uploads/2014/05/BflLogo.jpg" target="_blank" rel="noopener noreferrer"><img class="alignnone size-large wp-image-211" src="/images/uploads/2014/05/BflLogo-1024x121.jpg" alt="BflLogo" height="73" /></a></p>
<h2>Summary</h2>
This site is more of a landing page than an actual site. The purpose of this page was to give people an easy way to contact us to donate books or find out more about what we are doing.
<h2>Banner Design</h2>
Since the site is just one page, I figured if I got help with a decent banner, I could put together the rest of the site. I created a project on <a href="http://www.48hourslogo.com/" target="_blank" rel="noopener noreferrer">48HoursLogos</a> asking for help with designing the banner. Here is the initial page I created to help the designers see how simple this site really was. (The original name was TammyBook.com). I was only asking them for something to replace the TammyBook.com banner at the top.
<a href="/images/uploads/2014/05/TammyBook_com.png" target="_blank" rel="noopener noreferrer"><img class="alignleft size-large wp-image-226" src="/images/uploads/2014/05/TammyBook_com-1024x745.png" alt="TammyBook_com" height="454" /></a>
Here are some of the submissions I received.
<a href="/images/uploads/2015/05/Img07.png" target="_blank" rel="noopener noreferrer"><img class="size-large wp-image-225" src="/images/uploads/2015/05/Img07-1024x157.png" alt="Img07" height="95" /></a>
<a href="/images/uploads/2015/05/Img06.png" target="_blank" rel="noopener noreferrer"><img class="size-large wp-image-224" src="/images/uploads/2015/05/Img06-1024x158.png" alt="Img06" height="96" /></a>
<a href="/images/uploads/2015/05/Img05.png" target="_blank" rel="noopener noreferrer"><img class="alignright size-large wp-image-223" src="/images/uploads/2015/05/Img05-1024x148.png" alt="Img05" height="90" /></a>
<a href="/images/uploads/2015/05/Img04.png" target="_blank" rel="noopener noreferrer"><img class="alignright size-large wp-image-222" src="/images/uploads/2015/05/Img04-1024x119.png" alt="Img04" height="72" /></a>
I like them all and they each have their strengths. However, when I saw the last one with the children, I was hooked.  I had thought about a building block to convey the idea that this site is about children.  I worked with the designer, and he came up with the next version.
<p><a href="/images/uploads/2015/05/Img03.png" target="_blank" rel="noopener noreferrer"><img class="alignnone size-large wp-image-221" src="/images/uploads/2015/05/Img03-1024x116.png" alt="Img03" height="70" /></a>
This is closer, but the block didn't look like the classic building block I was thinking of. I found a picture on Google and uploaded it for him to see what I was thinking.</p>
<p>This was the final contest winner. He added the &quot;lines&quot; in the block and produced this.
Good design to me is like obscenity. I may not be able to describe it, but <a href="http://en.wikipedia.org/wiki/I_know_it_when_I_see_it" target="_blank" rel="noopener noreferrer">I know it when I see it</a>. This one just worked for me.
<a href="/images/uploads/2015/05/Img02.png" target="_blank" rel="noopener noreferrer"><img class="alignnone size-large wp-image-220" src="/images/uploads/2015/05/Img02-1024x116.png" alt="Img02" height="70" /></a></p>
<div class="page-break-before">
With his banner, the site currently looked like this. I thought it looked pretty good. The font used is Kristen ITC. It is intended to be a "child" font and considering the site, seemed right to me.  Here is what the site looked like with the new banner.</div>
<a href="/images/uploads/2015/05/BflWeb01.png"><img class="alignnone size-large wp-image-237" src="/images/uploads/2015/05/BflWeb01-1024x837.png" alt="BflWeb01" height="510" /></a>
<div class="page-break-before">
Since my designer had raised the bar of how the site should look, I asked if he would suggest any other changes on the site. He came up with this.</div>
<a href="/images/uploads/2015/05/BooksForLuke_140219a.jpg"><img class="alignnone size-large wp-image-238" src="/images/uploads/2015/05/BooksForLuke_140219a-975x1024.jpg" alt="BooksForLuke_140219a" height="656" /></a>
<p>I was &quot;nearly&quot; convinced. I liked the the new site. My only concern was that it may look a little &quot;too polished&quot; and we might lose the personal connection with the visitors. I submitted the two versions in a <a href="http://feedbackarmy.com/" target="_blank" rel="noopener noreferrer">FeedBack Army</a> project to get some feedback from other people. To my surprise, some people actually preferred the first site, but the majority liked the new site.</p>
<h2>Amazon Email</h2>
When a visitor fills out the form on the site, we receive an email <a href="http://docs.aws.amazon.com/ses/latest/DeveloperGuide/send-email-smtp.html" target="_blank" rel="noopener noreferrer">Using the Amazon SES SMTP Interface</a>.
<h2>Technology</h2>
<ul>
	<li>Asp.Net MVC, C#, Razor</li>
	<li>Amazon SES SMTP</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Health Level Seven (HL7) Application</title>
      <link href="https://scottbrooks.info/posts/health-level-seven-(hl7)-application/"/>
      <updated>2014-05-24T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/health-level-seven-(hl7)-application/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
Health Level Seven (HL7) is the standard for exchanging information between medical applications. This application receives HL7 messages from <a href="http://en.wikipedia.org/wiki/Hospital_information_system" target="_blank" rel="noopener noreferrer">Hospital Information Systems (HIS)</a>. These messages allow us to keep track of the location of each patient.
<h2>Description</h2>
If you have not seen HL7 before, it can look a little ugly the first time. Here is a message.
<p>[code]
MSH|^~&amp;|P2000|POH|TVIF||20100301185944|N|ADT^A01|01|T|2.3
EVN|A01|20100301185943|||scosby
PID|||399599|399599|FLINTSTONE^FRED^K||194807070000|M||W|2915 STONE LN^^WATERFORD^MI^48329||(248)111-2222|||||7000103191|777-88-9999
PV1||I|7E^7028^B||||01195^FLINTSTONE^PEBBLES^P|||REH||||||N|01195^FLINTSTONE^PEBBLES^P|3||T||||||||||||||||||||||||201003011857
GT1|||FLINTSTONE^FRED^K||2915 STONE LN^^WATERFORD^MI^48329^USA|(248)111-2222|||M
[/code]</p>
<div class="viewRaw">(For a detailed explanation, checkout <a href="http://en.wikipedia.org/wiki/Health_Level_7#HL7_version_2.x" target="_blank" rel="noopener noreferrer">this link</a>)</div>
You can see names, addresses, and phone numbers in the message.   The "7E^7028^B" is the assigned patient location (PointOfCare/Room/Bed).  HL7 is extremely detailed.  For births, it even identifies the birth order of each child.
Our software receives thousands of these messages per day.  I wrote the library code that breaks these messages into something our software can use.
<p>TODO: Code Samples and Screen shots.</p>
<h2>Technology</h2>
<ul>
	<li>C#</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>SSH Interface to Siemens HiPath 4000 Phone Switch</title>
      <link href="https://scottbrooks.info/posts/ssh-interface-to-siemens-hipath-4000-phone-switch/"/>
      <updated>2014-05-29T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/ssh-interface-to-siemens-hipath-4000-phone-switch/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
The purpose for this project was to change the <a href="http://en.wikipedia.org/wiki/Class_of_service#Voice_services" target="_blank" rel="noopener noreferrer">class of service</a> for a specific extension on a Siemens HiPath 4000 Phone Switch.  It connects with the switch over the network using SSH.
<h2>Description</h2>
The heavy lifting on this project is provided by the Jscape SSH component.  We had used their <a href="http://www.jscape.com/products/components/net/telnet-factory-for-net/" target="_blank" rel="noopener noreferrer">Telnet component</a> on another project, and found it very reliable and pretty easy to implement.
<p>TODO: Code Samples and Screen shots.</p>
<h2>Technology</h2>
<ul>
	<li>C#</li>
	<li><a href="http://www.jscape.com/products/components/net/ssh-factory-for-net/" target="_blank" rel="noopener noreferrer">SSH Factory for .NET (JScape)</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Visual Basic 6 Programming</title>
      <link href="https://scottbrooks.info/posts/visual-basic-6-programming/"/>
      <updated>2014-05-30T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/visual-basic-6-programming/</id>
      <content type="html">
        <![CDATA[
      <p>Visual Basic was fun.  I considered using VB2 for one project, but without database support, it was not the right fit.  When VB3 came along with database support, I was in.  I lived through the VB4 16/32 bit issues and fought the <a href="http://en.wikipedia.org/wiki/DLL_Hell" target="_blank" rel="noopener noreferrer">DLL Hell</a> that was just a part of life back then.  When VB6 finally came along and was so solid that the programs actually ran and did what we expected, it was very refreshing.  I programmed in Visual Basic for many years.</p>
<p>This is a small example of a function I wrote to be able to log messages that contained binary information.   A big part of our programming was serial communications, and the low ASCII chars wreaked havoc on the log files.<br>
I am not offering this as the perfect example of VB6 programming.  However, it does show that we had a convention for naming variables and parameters.  It also include error handling, which was automatically added and removed from a VB6 Addin I wrote (we called it CodeBuddy).</p>
<p>[code language=&quot;vb&quot;]</p>
<p>Public Function wb_ConvertFromBinary(pstrMsg As String) As String
On Error GoTo ErrorHandler:</p>
<p>Dim intLoop As Integer
Dim intLen As Integer
Dim strReturn As String
Dim strChar As String
Dim blnBinary As Boolean</p>
<p>intLen = Len(pstrMsg)
blnBinary = False</p>
<p>For intLoop = 1 To intLen</p>
<p>strChar = Mid(pstrMsg, intLoop, 1)
If Asc(strChar) &lt; 32 Then
blnBinary = True
strChar = &quot;&lt;&quot; &amp; Right(&quot;00&quot; &amp; Asc(strChar), 3) &amp; &quot;&gt;&quot;
End If
strReturn = strReturn &amp; strChar
DoEvents
Next intLoop</p>
<p>If blnBinary Then
strReturn = &quot;BIN:&quot; &amp; strReturn
End If</p>
<p>wb_ConvertFromBinary = strReturn</p>
<p>Exit Function
ErrorHandler:
wb_LogError &quot;WBSCommon/wb_ConvertFromBinary&quot;, Err.Description, Err.Number, Erl</p>
<p>End Function</p>
<p>[/code]</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Trello Interface for Daily Printed ToDo Page</title>
      <link href="https://scottbrooks.info/posts/trello-interface-for-daily-printed-todo-page/"/>
      <updated>2014-06-07T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/trello-interface-for-daily-printed-todo-page/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
<p>This project had two major goals. First, I wanted to filter <a href="http://www.trello.com" target="_blank" rel="noopener noreferrer">Trello</a> cards based on tags.
If you are not familiar with Trello, it is described as</p>
<div style="margin-left: 20px; margin-bottom: 6px; font-style: italic;">.. the fastest, easiest way to organize anything, from your day-to-day work, to a favorite side project, to your greatest life plans.</div>
It is an easy way to organize information into lists, which is how my mind works. <a href="https://fast.wistia.net/embed/iframe/zfss1tgtwl?autoPlay=true&amp;controlsVisibleOnLoad=true&amp;playerColor=006595&amp;popover=true&amp;version=v1&amp;videoHeight=540&amp;videoWidth=960" target="_blank" rel="noopener noreferrer">This video</a> gives a nice introduction.
<p>One current drawback with Trello is the lack of support for Tags. They do provide 6 different color coded labels. This is nice, but just not enough. Although I am not a perfect follower of the David Allen method of <a href="http://en.wikipedia.org/wiki/Getting_Things_Done" target="_blank" rel="noopener noreferrer">Getting Things Done</a>, I do try to organize my tasks using his methods.</p>
<p>In addition to adding a &quot;little GTD&quot; to Trello, I also wanted to output a page that was formatted for a printer. The idea is that I would print the page each morning and carry it with me throughout the day. I would mark it up, add notes and new tasks. I would draw a line through completed tasks and even have a place to <a href="http://en.wikipedia.org/wiki/Doodle" target="_blank" rel="noopener noreferrer">Doodle</a> during meetings. Then, at the end of the day or the next morning before I print a new page, I would update Trello with the notes from the paper.</p>
<h2>Description</h2>
One issue I am sure the Trello dev team has encountered with tags is the fact that cards are usually viewed by more than one user. Each user wants "their own" tags that makes sense to them. However, as a team, users may want a common set of tags.
<p>Here is my original list of tags:</p>
<ul>
 	<li>+call = Calls that need to be placed</li>
 	<li>+dead = Brain dead tasks for when we can’t focus.</li>
 	<li>+dash = Things that can be done in under 15 minutes</li>
 	<li>+email = Emails that need to be sent</li>
 	<li>+errand = Things we need to do/get while out</li>
 	<li>+focus = Requires dedicated time and concentration</li>
 	<li>+paper = Tasks that only require a pen & paper (Brainstorming)</li>
</ul>
The idea is that I would just enter "+call" anywhere in the description and then I could filter all the cards that contained that tag. This works for a single user, but one of the huge benefits of Trello is that other people are viewing and editing those same cards.
<p>The <a href="http://en.wikipedia.org/wiki/Kludge" target="_blank" rel="noopener noreferrer">Kludge</a> I came up with to solve this issue was to &quot;prefix&quot; the tag with the user initials. Now, my tag list became:</p>
<ul>
 	<li>+jsb-call = Calls that need to be placed</li>
 	<li>+jsb-email = Emails that need to be sent</li>
 	<li>+jsb-errand = Things we need to do/get while out</li>
 	<li>+jsb-focus = Requires dedicated time and concentration</li>
</ul>
(I dropped a few out that I was no longer using)
I add these tags in the card description. Now, I just needed an easy way to filter the cards. This is where the Trello REST API steps up.
<h3>Proof of Concept</h3>
For my <a href="http://en.wikipedia.org/wiki/Proof_of_concept" target="_blank" rel="noopener noreferrer">POC</a>, I created a Winforms applicaiton. Here is a screen capture:
<img class="alignnone size-full wp-image-493" src="/images/uploads/2015/05/DailyActions01.png" alt="" height="1570" />
<p>Yes, I went a little crazy with the colors, but I never claimed to be a designer (I can't even match a pair of socks). If it helps any, remember that the goal is for the page to be printed. That is the reason for the underscores before each task. The calendars don't really do anything on the web page, but are just there as a reference on the printed page. I also included the tag list as a reminder on the page.
<a href="#">top</a></p>
<h3>Page Template</h3>
I created an html template for the page. The body of the template is shown below. The CSS is also included in the page, but I left it out for this example.
<pre>[code language="html"]
&amp;lt;body&amp;gt;
    &amp;lt;div&amp;gt;
        &amp;lt;div style=&quot;float: right;&quot;&amp;gt;
            &amp;lt;div style=&quot;text-align: right;&quot;&amp;gt;
                &amp;lt;table&amp;gt;
                    &amp;lt;tr valign=&quot;top&quot;&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;span id=&quot;datePrior&quot; style=&quot;font-size: 7px;&quot;&amp;gt;&amp;lt;/span&amp;gt;
                        &amp;lt;/td&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;span id=&quot;dateNext&quot; style=&quot;font-size: 7px;&quot;&amp;gt;&amp;lt;/span&amp;gt;
                        &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                &amp;lt;/table&amp;gt;
                &amp;lt;table&amp;gt;
                    &amp;lt;tr valign=&quot;top&quot;&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;span id=&quot;dateCurrent&quot; style=&quot;font-size: 10px;&quot;&amp;gt;&amp;lt;/span&amp;gt;
                        &amp;lt;/td&amp;gt;
                        &amp;lt;td&amp;gt;
                            &amp;lt;div style=&quot;text-align: left;&quot;&amp;gt;
                                &amp;lt;span class=&quot;TagCaption&quot;&amp;gt;Tags&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                                &amp;lt;span class=&quot;TagList&quot;&amp;gt;+{{TagPrefix}}-call&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                                &amp;lt;span class=&quot;TagList&quot;&amp;gt;+{{TagPrefix}}-email&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                                &amp;lt;span class=&quot;TagList&quot;&amp;gt;+{{TagPrefix}}-errand&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                                &amp;lt;span class=&quot;TagList&quot;&amp;gt;+{{TagPrefix}}-focus&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                                &amp;lt;span class=&quot;TagList&quot;&amp;gt;+{{TagPrefix}}-next&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                                &amp;lt;span class=&quot;TagList&quot;&amp;gt;+{{TagPrefix}}-waiting&amp;lt;/span&amp;gt;&amp;lt;br /&amp;gt;
                            &amp;lt;/div&amp;gt;
                        &amp;lt;/td&amp;gt;
                    &amp;lt;/tr&amp;gt;
                &amp;lt;/table&amp;gt;
            &amp;lt;/div&amp;gt;
            &amp;lt;h2 class=&quot;CaptionCall&quot;&amp;gt;Appointments / Calls&amp;lt;/h2&amp;gt;
            {{call}}
            &amp;lt;h2 class=&quot;CaptionAssigned&quot;&amp;gt;Assigned To Me&amp;lt;/h2&amp;gt;
            {{@me}}
        &amp;lt;/div&amp;gt;
        &amp;lt;div style=&quot;float: left;&quot;&amp;gt;
            &amp;lt;h1&amp;gt;{{Date}}&amp;lt;span class=&quot;Time&quot;&amp;gt;{{Time}}&amp;lt;/span&amp;gt;&amp;lt;/h1&amp;gt;
            &amp;lt;h2 class=&quot;CaptionFocus&quot;&amp;gt;Focus&amp;lt;/h2&amp;gt;
            {{focus}}
            &amp;lt;h2 class=&quot;CaptionNext&quot;&amp;gt;Next Actions&amp;lt;/h2&amp;gt;
            {{next}}
            &amp;lt;h2 class=&quot;CaptionNote&quot;&amp;gt;Notes&amp;lt;/h2&amp;gt;
        &amp;lt;/div&amp;gt;
    &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
[/code]</pre>
<a href="#">top</a>
<h3>View Objects</h3>
I created my own objects that contain only the few properties I care about for my view.
<pre>[code language="csharp"]
<p>public class TrelloBoard
{
public string Id { get; set; }
public string Text { get; set; }
public string OrgText { get; set; }
}</p>
<p>public class TrelloList
{
public string Id { get; set; }
public string Text { get; set; }
}</p>
<p>public class TrelloCard
{
public string OrgText { get; set; }
public string BoardText { get; set; }
public string ListText { get; set; }
public string Text { get; set; }
public string Url { get; set; }
}</p>
<p>[/code]</pre></p>
<h3>CreatePage Method</h3>
Here is the method called to replace the tokens in the template. It looks for some specific tokens. If it finds a token that is not one of the pre-defined values, it calls the method to query Trello for that value.
<pre>[code language="csharp"]
private void CreatePage(ITrelloData data)
{
    try
    {
        string templateFile = Path.Combine(Application.StartupPath, &quot;DailyActions_Template.html&quot;);
        string htmlFile = Path.Combine(Application.StartupPath, &quot;DailyActions.html&quot;);
<p>string html = File.ReadAllText(templateFile);</p>
<p>string regEx = &quot;{{.*?}}&quot;;</p>
<p>MatchCollection tokenList = Regex.Matches(html, regEx, RegexOptions.IgnoreCase);</p>
<p>foreach (Match match in tokenList)
{
switch (match.Value.ToLower())
{
case &quot;{{date}}&quot;:
{
string date = dateTimePicker1.Value.ToLongDateString();
html = html.Replace(match.Value, date);
break;
}
case &quot;{{time}}&quot;:
{
string time = dateTimePicker1.Value.ToLongTimeString();
html = html.Replace(match.Value, time);
break;
}
case &quot;{{tagprefix}}&quot;:
{
html = html.Replace(match.Value, txtTagPrefix.Text);
break;
}
default:
{
List&amp;lt;TrelloCard&amp;gt; cards = data.GetCardsForQuery(match.Value);
html = html.Replace(match.Value, GetActionsHtml(cards));
break;
}
}
}</p>
<p>File.WriteAllText(htmlFile, html);</p>
<p>webBrowser1.Navigate(htmlFile);
}
catch (Exception ex)
{
Console.WriteLine(&quot;Exception: &quot; + ex.ToString());
}
}
[/code]</pre>
<a href="#">top</a></p>
<h3>GetCardsForQuery Method</h3>
<pre>[code language="csharp"]
public List&amp;lt;TrelloCard&amp;gt; GetCardsForQuery(string token)
{
    // Returns an ordered list of Trello cards that have only the
    // properties from the the Board and List that are required
    // for the view.
<p>// Remove the token delimiters.
string query = token.Replace(&quot;{&quot;, &quot;&quot;).Replace(&quot;}&quot;, &quot;&quot;);</p>
<p>// Trello uses the &quot;@&quot; for specific queries.  If this is not one of
// those queries, we need to add the tag prefix (user initials) and
// surround the query with quotes.
if (!query.Contains(&quot;@&quot;))
{
query = &quot;&quot;&quot; + &quot;+&quot; + _tagPrefix + &quot;-&quot; + query + &quot;&quot;&quot;;
}</p>
<p>List&amp;lt;TrelloCard&amp;gt; retVal = new List&amp;lt;TrelloCard&amp;gt;();
List&amp;lt;TrelloBoard&amp;gt; boards = new List&amp;lt;TrelloBoard&amp;gt;();
List&amp;lt;TrelloList&amp;gt; lists = new List&amp;lt;TrelloList&amp;gt;();</p>
<p>IEnumerable&amp;lt;Card&amp;gt; cards = _trello.Cards.Search(query, 1000);</p>
<p>foreach (var card in cards)
{
TrelloCard trelloCard = new TrelloCard
{
Url = card.Url,
Text = card.Name
};</p>
<p>TrelloBoard trelloBoard = boards.Find(b =&amp;gt; b.Id == card.IdBoard);
if (trelloBoard == null)
{
trelloBoard = new TrelloBoard { Id = card.IdBoard };
Board board = _trello.Boards.WithId(card.IdBoard);
trelloBoard.Text = board.Name;
trelloBoard.OrgText = _trello.Organizations.WithId(board.IdOrganization).DisplayName;
boards.Add(trelloBoard);
}
trelloCard.BoardText = trelloBoard.Text;
trelloCard.OrgText = trelloBoard.OrgText;</p>
<p>TrelloList list = lists.Find(b =&amp;gt; b.Id == card.IdList);
if (list == null)
{
list = new TrelloList { Id = card.IdList };
list.Text = _trello.Lists.WithId(card.IdList).Name;
lists.Add(list);
}
trelloCard.ListText = list.Text;</p>
<p>retVal.Add(trelloCard);
}</p>
<p>retVal = retVal.OrderBy(o =&amp;gt; o.OrgText + &quot;-&quot; + o.BoardText + &quot;-&quot; + o.Text).ToList();</p>
<p>return retVal;
}
[/code]</pre>
<a href="#">top</a></p>
<h3>GetActionsHtml Method</h3>
Returns an Html unordered list grouped by Organization and Board.
<pre>[code language="csharp"]
private string GetActionsHtml(List&amp;lt;TrelloCard&amp;gt; actionList)
{
    StringBuilder sb = new StringBuilder();
<p>string org = &quot;&quot;;
string board = &quot;&quot;;
foreach (TrelloCard nextAction in actionList)
{
if (nextAction.OrgText != org)
{
if (!string.IsNullOrEmpty(org))
{
sb.Append(&quot;&amp;lt;/ul&amp;gt;&quot;);
board = &quot;&quot;; // Since the Org is changing, clear the board.
}
sb.AppendFormat(&quot;&amp;lt;div class='Organization'&amp;gt;{0}&amp;lt;/div&amp;gt;&quot;, nextAction.OrgText);
sb.AppendLine();</p>
<p>if (!string.IsNullOrEmpty(board))
{ sb.Append(&quot;&amp;lt;/ul&amp;gt;&quot;); }
sb.AppendFormat(&quot;&amp;lt;div class='Board'&amp;gt;{0}&amp;lt;/div&amp;gt;&quot;, nextAction.BoardText);</p>
<p>sb.AppendLine();
sb.Append(&quot;&amp;lt;ul&amp;gt;&quot;);</p>
<p>org = nextAction.OrgText;
board = nextAction.BoardText;
}
sb.AppendFormat(&quot;&amp;lt;li&amp;gt;_ &amp;lt;a href='{0}' target='trello'&amp;gt;{1}&amp;lt;/a&amp;gt;&amp;lt;span class='List'&amp;gt;{2}&amp;lt;/span&amp;gt;&amp;lt;/li&amp;gt;&quot;,
nextAction.Url, nextAction.Text, nextAction.ListText);
sb.AppendLine();
}
sb.Append(&quot;&amp;lt;/ul&amp;gt;&quot;);
sb.AppendLine();</p>
<p>return sb.ToString();
}
[/code]</pre>
<a href="#">top</a></p>
<h2>Future Plans</h2>
<ul>
 	<li>Convert to Asp.Net MVC site, allow others to use it.</li>
 	<li>Schedule page to be ran daily and emailed</li>
</ul>
<h2>Technology</h2>
<ul>
 	<li>.Net Winform, C#</li>
 	<li><a title="Trello.NET API" href="https://github.com/dillenmeister/Trello.NET" target="_blank" rel="noopener noreferrer">Trello.NET API</a></li>
</ul>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Footer for all pages on site</title>
      <link href="https://scottbrooks.info/posts/footer-for-all-pages-on-site/"/>
      <updated>2014-10-11T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/footer-for-all-pages-on-site/</id>
      <content type="html">
        <![CDATA[
      <h1>Summary</h1>
<p>During the testing portion of a recent server move, I wanted to be sure the user knew they were on the test server. It seemed simple enough, and the solution I ended up with was not difficult.</p>
<p>There was one &quot;critical&quot; piece that probably cost me an extra couple hours, and it turned out to be one of those &quot;one liners&quot;. It is the second line in the snippet below (urlCompression...)</p>
<h2>Example</h2>
<img class="alignnone wp-image-445 size-large" src="/images/uploads/2014/10/SiteFooter-1024x687.png" alt="" height="419" />
I realize the orange has a certain "Home Depot" look to it, but I wanted it to be clear for the user. I even got to use a marquee for the first time in more than 10 years (they were hot, back in the day).
<p><a title="This article" href="http://www.iis.net/learn/extensions/url-rewrite-module/creating-outbound-rules-for-url-rewrite-module" target="_blank" rel="noopener noreferrer">This article</a> is a step-by-step with screen shots, but I was getting an Error 500 when I enable the rule.</p>
<p>I added this to the bottom of the web.confg files.</p>
<pre>[code language="CSharp"]
    &amp;lt;system.webServer&amp;gt;
     &amp;lt;urlCompression doDynamicCompression=&quot;false&quot; /&amp;gt;
        &amp;lt;rewrite&amp;gt;
            &amp;lt;outboundRules&amp;gt;
                &amp;lt;rule name=&quot;AddPageFooter&quot; preCondition=&quot;IsHtml&quot; enabled=&quot;true&quot;&amp;gt;
                    &amp;lt;match filterByTags=&quot;None&quot; pattern=&quot;&amp;amp;lt;/body&amp;gt;&quot; /&amp;gt;
                    &amp;lt;action type=&quot;Rewrite&quot; value=&quot;&amp;amp;lt;marquee loop=&amp;amp;quot;1&amp;amp;quot; behavior=&amp;amp;quot;alternate&amp;amp;quot; style='background-color: #ff5500; color: white; width=100% text-align: center; font-size: 14pt; padding: 5px;'&amp;amp;gt;Test Site (not Home Depot)&amp;amp;lt;/marquee&amp;amp;gt;&quot; /&amp;gt;
                &amp;lt;/rule&amp;gt;
                &amp;lt;preConditions&amp;gt;
                    &amp;lt;preCondition name=&quot;IsHtml&quot;&amp;gt;
                        &amp;lt;add input=&quot;{RESPONSE_CONTENT_TYPE}&quot; pattern=&quot;^text/html&quot; /&amp;gt;
                    &amp;lt;/preCondition&amp;gt;
                &amp;lt;/preConditions&amp;gt;
                &amp;lt;customTags&amp;gt;
                    &amp;lt;tags name=&quot;body&quot; /&amp;gt;
                &amp;lt;/customTags&amp;gt;
            &amp;lt;/outboundRules&amp;gt;
        &amp;lt;/rewrite&amp;gt;
    &amp;lt;/system.webServer&amp;gt;
[/code]</pre>
<p>Here is the html I used for the message before I encoded it (using the
<a title="string encoder" href="http://www.whatsmyip.org/string-encoder-decoder/" target="_blank" rel="noopener noreferrer">string encoder</a> at http://www.whatsmyip.org)
I believe the &quot;text-align&quot; was a holdover from where I started with a div. It should probably come out, but this is already deployed, so I will leave it.</p>
<pre>[code language="html"]

&amp;lt;marquee loop=&quot;1&quot; behavior=&quot;alternate&quot; style='background-color: #ff5500;
color: white; width=100% text-align: center; font-size: 14pt;
padding: 5px;'&amp;gt;Test Site (not Home Depot)&amp;lt;/marquee&amp;gt;

[/code]</pre>
<p>Example
Test Site (not Home Depot)</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Sql Server Stored Procedure Template</title>
      <link href="https://scottbrooks.info/posts/sql-server-stored-procedure-template/"/>
      <updated>2015-07-12T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/sql-server-stored-procedure-template/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
Have you ever written that "perfect" piece of code to handle some situation?  You spend hours reading blogs and other code, and you finally pull it all together in a version that works in your project.   In your mind, you know that since you just spent a few hours on this, there is no way you are going to forget it.  In a future project when you will need this same code, you will just "remember" it was in xyz project.  Well, for me, I start out that way, but I soon realize that I can't remember where I did it.  To my credit, I usually at least remember that I did it, I just don't know where.  While I love <a href="https://www.baremetalsoft.com/" target="_blank" rel="noopener noreferrer">BareGrep</a> as much as anybody, it is a bit like Google.  If you don't enter the right search terms, you still don't find it.
<p>So this post (and future posts like it) are an attempt to put that code in &quot;one place&quot; where I hope to be able to find it down the road.   If the chunk of code is not worth a blog post, then it must not have been that impressive after all. :-)</p>
<p>This morning, I was trying to to implement error handling in my catch block.  I found a chunk of code I had used in the past, and it was rolling back transactions, which reminded me that I had forgot to put that in the current sproc I was working on.  At that point, I started this post.  Below are some notes that should be helpful to me in the future.  I hope they will benefit you as well.</p>
<h2>Header Comment</h2>
Include some kind of comment. I include a PRINT statement, which helps in the SqlCmd (future post) script that calls this.   This also includes the DROP/CREATE approach which I like to do for sprocs.
<p>[code language=&quot;sql&quot;]
/*
Revision History
4Jul15  ScottB  Initial Version
*/
PRINT 'ProceduresCall_spt.sql'</p>
<p>SET ANSI_NULLS ON
GO</p>
<p>SET ANSI_NULLS ON
GO</p>
<p>-- http://technet.microsoft.com/en-us/library/ms190324(v=sql.100).aspx
IF (OBJECT_ID(N'call.Call_spt', 'P') IS NOT NULL ) BEGIN
DROP PROCEDURE call.Call_spt
PRINT '   Dropped';
END
GO</p>
<p>SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE call.Call_spt
@xml XML
AS
...
[/code]
<a href="#">top</a></p>
<h2> Initial Variables</h2>
Declare a couple variables at the very beginning that will be needed later. The @tranCount is needed to be able to commit and rollback transactions. The @tenantId is needed in the Error handler, so we set it outside the BEGIN TRY (not sure if this is necessary in TSQL, but it would be in C#).
[code language="sql"]
BEGIN
	SET NOCOUNT ON;
<p>DECLARE @tranCount INT = @@TRANCOUNT;
-- Default to System, but it should get reset below.
DECLARE @tenantId INT = 1;
[/code]
<a href="#">top</a></p>
<h2>BEGIN TRANSACTION ?? / COMMIT</h2>
As close as possible to your actual Insert or Update, you need a block like this.  It handles the situation where we were already in a transaction (possibly from a calling sproc).
[code language="sql"]
IF (@tranCount = 0) BEGIN
   BEGIN TRANSACTION
END ELSE BEGIN
   SAVE TRANSACTION Call_spt
END
<p>-- Insert or Update ...</p>
<p>IF (@tranCount = 0) BEGIN
COMMIT TRANSACTION
END</p>
<p>[/code]
<a href="#">top</a></p>
<h2>Catch Block</h2>
This next block is assuming that the prior code was in a TRY CATCH block.
<p>[code language=&quot;sql&quot;]
BEGIN CATCH</p>
<p>DECLARE @xstate INT = XACT_STATE();
IF @xstate = -1 BEGIN
ROLLBACK TRANSACTION
END
IF @xstate = 1 AND @tranCount = 0 BEGIN
ROLLBACK TRANSACTION
END
IF @xstate = 1 AND @tranCount &gt; 0 BEGIN
ROLLBACK TRANSACTION Call_spt
END</p>
<p>PRINT 'Error ' + CONVERT(varchar(50), ERROR_NUMBER()) +
', Severity ' + CONVERT(varchar(5), ERROR_SEVERITY()) +
', State ' + CONVERT(varchar(5), ERROR_STATE()) +
', Procedure ' + ISNULL(ERROR_PROCEDURE(), '-') +
', Line ' + CONVERT(varchar(5), ERROR_LINE());
PRINT ERROR_MESSAGE();</p>
<p>EXEC tenant.ReThrowError_spt @tenantId</p>
<p>RETURN -1;
END CATCH	
[/code]
<a href="#">top</a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>NoSql, YesSql, MaybeSql - A hybrid approach</title>
      <link href="https://scottbrooks.info/posts/nosql-yessql-maybesql-a-hybrid-approach/"/>
      <updated>2015-07-21T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/nosql-yessql-maybesql-a-hybrid-approach/</id>
      <content type="html">
        <![CDATA[
      <p>UPDATE: I don't usually claim something as an original idea.  Anything formed in my brain is just an amalgamation of the videos I have watched and the stuff I have read.   For this idea of just creating a JSON field in each table, I don't remember seeing it anywhere else before.  That is why I thought it was neat when I was perusing the Octopus tables today, and I noticed they also include a JSON field in each table.  They even went as far as to make theirs Not Null, which I didn't do.
<a href="/images/uploads/2016/02/OctopusJson.png"><img src="/images/uploads/2016/02/OctopusJson.png" alt="OctopusJson" height="869" class="alignnone size-full wp-image-939" /></a></p>
<p>Others can argue when to use Sql vs a NoSql solution. There is a time and place for each. I have went down both paths and there usually comes a time where the grass looks a little greener on the other side. Sql is great for creating related tables and helping to ensure referential integrity at the database level. However, it can be a little cumbersome to add one simple field to an existing table. Even if you are doing migrations from code, there is some work involved.</p>
<p>On the other hand, NoSql is great at handling this &quot;jagged&quot; data. You simple add new properties to your json object and store it. However, the problem I have had with NoSql in the past is that you have to put more thought into your data design up front to know how you will want to pull it out. Often, ad hoc queries require a full table scan. Some of these things may no longer be true, but who can keep up with how quickly things change around us?</p>
<p>Anyway, in some recent work, I stumbled across an approach that I think combines the benefits of both approaches. Others have probably been doing some version of this for years, but it just clicked for me the other day. The &quot;trick&quot; is to create a VARCHAR(max) field where we can store a json object. This gives me most of the relational goodness of Sql along with the ability to easily add &quot;minor&quot; fields, without touching the database.  This will not work for &quot;major&quot; fields that are a key to another table or something that is going to be searched often.  However, for something like adding a link to store the gravatar on a user record, this can be pretty sweet.</p>
<p>Finally, some code. Here is the script I use to create a table. The key is line 21, where I add my <strong>JsonData</strong> field. Any table that might need &quot;extended&quot; in the future gets this new field.</p>
<p><a href="/images/uploads/2015/07/150721_SqlPerson.png"><img class="alignnone size-full wp-image-661" src="/images/uploads/2015/07/150721_SqlPerson.png" alt="150721_SqlPerson" height="749" /></a></p>
<p>Here is the C# code for the Person object.</p>
<p>[code language=&quot;csharp&quot;]
public class Person : EntityBase
{
public long PersonId { get; set; }
public long TenantId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }</p>
<p>public Person_NoSql NoSql { get; set; }</p>
<p>public Person()
{
NoSql = new Person_NoSql();
}</p>
<p>public string JsonData
{
get { return JsonConvert.SerializeObject(NoSql); }
set { NoSql = JsonConvert.DeserializeObject&lt;Person_NoSql&gt;(value); }
}</p>
<p>public class Person_NoSql
{
public string UrlAvatar { get; set; }
public string AnyOtherField { get; set;}
}
}
[/code]</p>
<p>The code to use the new property is pretty simple.
[code language=&quot;csharp&quot;]
Person person = mgr.GetPerson(personId);</p>
<p>if (!ReferenceEquals(null, person))
{<br>
FirstName = person.FirstName;
LastName = person.LastName;</p>
<p>UrlAvatar = person.NoSql.UrlAvatar;
AnyOtherField = person.NoSql.AnyOtherField;
}
[/code]</p>
<p>I am using Dapper for the data layer.  When it sees a JsonData field returned from a query, it maps it to the JsonData string property on the object.  As far as Dapper knows, this is just another string field.</p>
<p>That is pretty much it.  I thought this would be a longer post, but in the words of the great &quot;Forrest Gump&quot;, <a href="https://www.youtube.com/watch?v=Otm4RusESNU" target="_blank" rel="noopener noreferrer"> That's all I have to say about that</a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Azure, SignalR, &amp; Redis File Validation Site</title>
      <link href="https://scottbrooks.info/posts/azure-signalr-and-redis-file-validation-site/"/>
      <updated>2015-07-22T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/azure-signalr-and-redis-file-validation-site/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
This project was to develop a site to validate files.  The user will drop a .csv file on the page (or click the Browse button to find a file).  After the file is uploaded, it will be analyzed to determine if it matches the expected format.
<p>This was an Azure project from the beginning, which made it fun.  I was also able to use SignalR to provide immediate feedback to the user as the file was being analyzed.  I use Redis on the server to store the results for the last 7 days.  Purging old results is automatic, since Redis can expire a key after a set number of days.</p>
<p>The site uses FluentValidation for the many rules to check the file.  This tool made some pretty complex validation much easier.  It also helped eliminate what would have been a bunch of repetitious code.</p>
<h2>Screens</h2>
<h3>Main Screen</h3>
When the user loads the site, this is the first screen they see.  They can either drop a file on this page or click the browse button for a file picker dialog.  
The User textbox and associated button was a temporary way of grouping SignalR messages.  The name really should have been changed to Group instead of user.  The idea is that everybody listening in Group "Fred" would receive the same messages.  This is important because of the next screen.
<p><a href="/images/uploads/2015/07/150722_Clean.png"><img src="/images/uploads/2015/07/150722_Clean.png" alt="150722_Clean" height="1080" class="alignnone size-full wp-image-682" /></a></p>
<h3>Receiving File Screen</h3>
As the file is being analyzed, results are being sent back to the browser.  The top 5 errors encountered are entered in the green list (not sure why I made it green).  The gauge animates to show the number of lines processed.
<a href="/images/uploads/2015/07/150722_ErrorsGreen.png"><img src="/images/uploads/2015/07/150722_ErrorsGreen.png" alt="150722_ErrorsGreen" height="1080" class="alignnone size-full wp-image-683" /></a>
<h3>Upload Complete Screen</h3>
When the analysis is complete, the user will either see a green check mark indicating the file is good, or they will see the green check mark with the red "not" circle indicating there were problems.  A DataTables grid appears at the bottom of the page where they can view all the errors (using paging).
<p><a href="/images/uploads/2015/07/150722_Failure.png"><img src="/images/uploads/2015/07/150722_Failure.png" alt="150722_Failure" height="1080" class="alignnone size-full wp-image-677" /></a></p>
<h3>History List Screen</h3>
The user can view the history for the last 7 days.  This screen had as much value to me as the developer as it did to the user.  Before I added this screen, I would get an email saying some file didn't validate.  The email was not very descriptive as to why it didn't validate and I was left searching log messages to figure out the problem.   This screen allowed me to easily see why a file failed validation.  I added a date picker to select the day and view the results.
<a href="/images/uploads/2015/07/150722_HistoryList.png"><img src="/images/uploads/2015/07/150722_HistoryList.png" alt="150722_HistoryList" height="1080" class="alignnone size-full wp-image-679" /></a>
<h3>History Detail Screen</h3>
Once a file is clicked from the History List Screen, this screen displays the detailed error messages for that file.
<a href="/images/uploads/2015/07/150722_HistoryGrid.png"><img src="/images/uploads/2015/07/150722_HistoryGrid.png" alt="150722_HistoryGrid" height="1080" class="alignnone size-full wp-image-678" /></a>
<h2>Code</h2>
<h3>Code Coverage</h3>
I know how rare it is to get 97% code coverage.  I have been on projects where we were happy with 75-80% (and many where we had none).  This was really helpful when we needed to do a version 1.1 of this project.  I made the required changes, ran my tests.  Some of them were broke, as they should have been, based on the new requirements.  I fixed those tests and added a few more.
I highlighted the catch block above, since that is the code that is hardest for me to get code coverage on.  I know many people would eliminate the try/catch and let it bubble up.  That would "fix" the code coverage issue, but I am not really sure that is a fix.  In my experience, I like to catch the error as close to where it happened as possible.  Often, I can add parameters that were passed to the method in the error handler so I know what value it was working with when the error occurred.
<p><a href="/images/uploads/2015/07/CodeCover_141027.png"><img src="/images/uploads/2015/07/CodeCover_141027.png" alt="CodeCover_141027" height="583" class="alignnone size-full wp-image-680" /></a></p>
<h3>Code Execution Time</h3>
We all agree that our unit tests should be fast so they can be run often.  However, this was another one of those rare cases that I don't see on most projects.  It ran all 130 tests in under 2 seconds.
<a href="/images/uploads/2015/07/TestsPassed_141002_1130.png"><img src="/images/uploads/2015/07/TestsPassed_141002_1130.png" alt="TestsPassed_141002_1130" height="1143" class="alignnone size-full wp-image-681" /></a>
<h2>Technology / Tools</h2>
<ul>
   <li>C#</li>
   <li>SignalR</li>
   <li>Redis</li>
   <li>Azure</li>
   <li>FluentValidation</li>
   <li>AngularJS</li>
   <li>DataTables</li>
   <li>DropZone</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Go/NoGo Dashboard for System Health</title>
      <link href="https://scottbrooks.info/posts/gonogo-dashboard-for-system-health/"/>
      <updated>2016-01-28T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/gonogo-dashboard-for-system-health/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
The idea for this project was to create a "Health" dashboard similar to <a href="http://status.aws.amazon.com/" target="_blank" rel="noopener noreferrer">Amazon</a> or <a href="https://azure.microsoft.com/en-us/status/" target="_blnak" rel="noopener noreferrer">Azure</a>. In our environment, we don't seem to have a problem getting emails when things fail.
<p>Our problem was that were not getting notified about events that should have happened but did not. For example, if the process to import a file normally runs every morning, but it does not run one morning, we didn't know about it. Since it didn't run, it did not send an email about any problem.</p>
<p>Another benefit of a health dashboard is for a quick &quot;sanity check&quot; when a problem is reported. As a first step, we can check the dashboard to see the overall health of the system.</p>
<h2>Screens</h2>
<h3>Home Page</h3>
The main page allows the user to quickly filter the checks by the environment (Prod, Test, Dev).
<a href="/images/uploads/2016/01/Gng01.png"><img class="alignnone size-full wp-image-793" src="/images/uploads/2016/01/Gng01.png" alt="Gng01" height="1037" /></a>
<h3>Home Page - Dark</h3>
I am not a designer (I can barely match a pair of socks). However, I do like options. Bootstrap/<a href="https://bootswatch.com/" target="_blank" rel="noopener noreferrer">BootSwatch</a> made it easy to do that. I allow the user to pick the theme they like, simply by clicking the links in the lower right of the screen.
<a href="/images/uploads/2016/01/Gng01b.png"><img class="alignnone size-full wp-image-794" src="/images/uploads/2016/01/Gng01b.png" alt="Gng01b" height="970" /></a>
<h3>Dashboard filtered by tag</h3>
The system supports tags to make it easy to group related checks. For example, all tasks running on a specific server may be tagged with that server name.
<a href="/images/uploads/2016/01/Gng02.png"><img class="alignnone size-full wp-image-792" src="/images/uploads/2016/01/Gng02.png" alt="Gng02" height="1032" /></a>
<h3>Edit Task &amp; Tags</h3>
Example of screen to edit task description and tags along with any notes for the specific check.
<a href="/images/uploads/2016/01/Gng03.png"><img class="alignnone size-full wp-image-791" src="/images/uploads/2016/01/Gng03.png" alt="Gng03" height="1028" /></a>
<h3>Show Triggers</h3>
Each check can have multiple triggers. This allows some pretty flexible scheduling as shown in this example.
<a href="/images/uploads/2016/01/Gng04.png"><img class="alignnone size-full wp-image-790" src="/images/uploads/2016/01/Gng04.png" alt="Gng04" height="1031" /></a>
<h3>Edit Trigger</h3>
Clicking a trigger in the list brings up the modal edit dialog. I tried to use buttons to make it easy and visual for the user to "see" the schedule.
<a href="/images/uploads/2016/01/Gng05.png"><img class="alignnone size-full wp-image-789" src="/images/uploads/2016/01/Gng05.png" alt="Gng05" height="1027" /></a>
<h3>View Actions</h3>
<a href="/images/uploads/2016/01/Gng06.png"><img class="alignnone size-full wp-image-810" src="/images/uploads/2016/01/Gng06.png" alt="Gng06" height="1027" /></a>
<h3>Edit SQL Action</h3>
Example dialog to edit the Sql used for the check.
<a href="/images/uploads/2016/01/Gng07.png"><img class="alignnone size-full wp-image-787" src="/images/uploads/2016/01/Gng07.png" alt="Gng07" height="1032" /></a>
<h3>History Tab</h3>
The helps determine how often a check is failing. It also includes any notes that were added about a failure.
<a href="/images/uploads/2016/01/Gng08.png"><img class="alignnone size-full wp-image-786" src="/images/uploads/2016/01/Gng08.png" alt="Gng08" height="1031" /></a>
<h3>Edit Event Note</h3>
Clicking the pencil in the far right column allows the user to enter a note about a specific event. This can be helpful to explain why some event failed.
<a href="/images/uploads/2016/01/Gng09.png"><img class="alignnone size-full wp-image-785" src="/images/uploads/2016/01/Gng09.png" alt="Gng09" height="1029" /></a>
<h3>Export to Excel</h3>
This serves two purposes.
<ol>
<li>Backup of existing configuration.</li>
<li>I didn't complete this piece, but the idea was to be able to add checks to the spreadsheet and import it back into the system.</li>
</ol>
<a href="/images/uploads/2016/01/Gng10.png"><img src="/images/uploads/2016/01/Gng10.png" alt="Gng10" height="1029" class="alignnone size-full wp-image-784" /></a>
<h3>WebAPI Generated Doc</h3>
Here is an example of the API Doc we get for free with WebAPI.
<a href="/images/uploads/2016/01/Gng11.png"><img src="/images/uploads/2016/01/Gng11.png" alt="Gng11" height="1028" class="alignnone size-full wp-image-783" /></a>
<h3>Trello Punch List</h3>
Trello screenshot showing a portion of the project punch list.
<a href="/images/uploads/2016/01/Gng12.png"><img src="/images/uploads/2016/01/Gng12.png" alt="Gng12" height="1028" class="alignnone size-full wp-image-782" /></a>
<h2>Technology / Tools</h2>
<ul>
	<li>C#</li>
	<li>WebApi</li>
	<li>AngularJS</li>
	<li>Bootstrap/<a href="https://bootswatch.com/" target="_blank" rel="noopener noreferrer">BootSwatch</a></li>
	<li>Windows Service</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Senior Software Developer - Resurgent Capital Services</title>
      <link href="https://scottbrooks.info/posts/senior-software-developer-resurgent-capital-services/"/>
      <updated>2016-01-28T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/senior-software-developer-resurgent-capital-services/</id>
      <content type="html">
        <![CDATA[
      <p><strong>Jun 2014 - Present</strong></p>
<ul style="color:#000000">
<li>Created a <b><a href="/posts/azure-signalr-and-redis-file-validation-site/">File Validation</a></b> site using Azure, SignalR, & Redis.  Project included unit tests and I was able to obtain 97% code coverage.</li>
<li>Developed a <b><a href="/posts/gonogo-dashboard-for-system-health/">Go/NoGo Dashboard</a></b> to provide a "quick glance" of system health.  The site includes an AngularJS website, WebAPI service layer, and a Windows Service to perform the system checks.</li>
<li>Created a simple <b><a href="/posts/markdown-site-(wiki)-to-share-project-information/">Wiki site</a></b> to display and edit Markdown to help document projects and share information.</li>
<li>Integrated <b><a href="/posts/integrate-database-migrations-with-octopus-deploy/">Database Migrations</a></b> with Octopus Deploy</li>
<li>Installed <b><a href="/images/uploads/2016/01/Discourse01.png" target="_blank" rel="noopener noreferrer">Discourse</a></b> in Azure in attempt to foster group discussions. (Jury is still out on people using the tool)</li>
<li>In addition to the fun projects listed above, I have also helped maintain existing legacy applications. (Some of the apps have 12+ year old stored procedures that still run hundreds of times per day)</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Sql Code Snippets</title>
      <link href="https://scottbrooks.info/posts/sql-code-snippets/"/>
      <updated>2016-01-28T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/sql-code-snippets/</id>
      <content type="html">
        <![CDATA[
      <h3>Creating a custom snippet</h3>
<h4>Create a Snippets folder somewhere (I like c:SqlSnippets)</h4>
<a href="/images/uploads/2016/01/Sql01.png"><img class="alignnone size-full wp-image-721" src="/images/uploads/2016/01/Sql01.png" alt="Sql01" height="399" /></a>
<h4>Use the following template as your first snippet</h4>
<pre>[code language="xml"]
&amp;lt;?xml version=&quot;1.0&quot; encoding=&quot;utf-8&quot; ?&amp;gt;
&amp;lt;CodeSnippets  xmlns=&quot;http://schemas.microsoft.com/VisualStudio/2005/CodeSnippet&quot;&amp;gt;
  &amp;lt;CodeSnippet Format=&quot;1.0.0&quot;&amp;gt;
    &amp;lt;Header&amp;gt;
      &amp;lt;Title&amp;gt;Logging - Standard Template&amp;lt;/Title&amp;gt;
      &amp;lt;Shortcut&amp;gt;&amp;lt;/Shortcut&amp;gt;
      &amp;lt;Description&amp;gt;Insert script to read Logging&amp;lt;/Description&amp;gt;
      &amp;lt;Author&amp;gt;Company Name Here&amp;lt;/Author&amp;gt;
      &amp;lt;SnippetTypes&amp;gt;
        &amp;lt;SnippetType&amp;gt;Expansion&amp;lt;/SnippetType&amp;gt;
      &amp;lt;/SnippetTypes&amp;gt;
    &amp;lt;/Header&amp;gt;
    &amp;lt;Snippet&amp;gt;
      &amp;lt;Code Language=&quot;SQL&quot;&amp;gt;
        &amp;lt;![CDATA[
 SELECT
        DATEADD(hour, DATEDIFF(hour, GETUTCDATE(), GETDATE()), TimeStamp) AS RunTime
      , Message
      , UserName
      , . . .
 FROM Logging.dbo.LogTable WITH (NOLOCK)
WHERE TimeStamp &amp;gt; DateAdd(MINUTE, -30, GetUtcDate())
ORDER BY TimeStamp
]]&amp;gt;
      &amp;lt;/Code&amp;gt;
    &amp;lt;/Snippet&amp;gt;
  &amp;lt;/CodeSnippet&amp;gt;
&amp;lt;/CodeSnippets&amp;gt;
[/code]</pre>
<h4>Open Code Snippet Manager (^KB)</h4>
<a href="/images/uploads/2016/01/Sql02.png"><img class="alignnone size-full wp-image-724" src="/images/uploads/2016/01/Sql02.png" alt="Sql02" height="467" /></a>
<ul>
 	<li>Click <b>Add</b> button</li>
 	<li>Navigate to your new Snippets folder</li>
 	<li>Click the <b>Select Folder</b> button</li>
</ul>
<h4>Expand your Snippets Folder</h4>
<a href="/images/uploads/2016/01/Sql03.png"><img class="alignnone size-full wp-image-727" src="/images/uploads/2016/01/Sql03.png" alt="Sql03" height="456" /></a>
<h3>Cleaning up the Insert Snippet Dialog</h3>
<h4>Insert Snippet in Query window</h4>
<ul>
 	<li>Press ^KX to bring up the Insert snippet window (before our change)</li>
</ul>
<a href="/images/uploads/2016/01/Sql04.png"><img class="alignnone size-full wp-image-729" src="/images/uploads/2016/01/Sql04.png" alt="Sql04" height="596" /></a>
<p>This dropdown is too busy and it is hard to find our custom snippets.</p>
<ul>
 	<li>Go back to the Code Snippets Manager (^KB)</li>
</ul>
<a href="/images/uploads/2016/01/Sql05.png"><img class="alignnone size-full wp-image-731" src="/images/uploads/2016/01/Sql05.png" alt="Sql05" height="459" /></a>
<ul>
 	<li>Open the folder shown in the Location textbox.</li>
 	<li>Make a backup folder to save these scripts and move all files into the backup folder</li>
</ul>
<h4>Folder before backup</h4>
<a href="/images/uploads/2016/01/Sql06.png"><img class="alignnone size-full wp-image-733" src="/images/uploads/2016/01/Sql06.png" alt="Sql06" height="571" /></a>
<h4>Folder after backup</h4>
<a href="/images/uploads/2016/01/Sql07.png"><img class="alignnone size-full wp-image-734" src="/images/uploads/2016/01/Sql07.png" alt="Sql07" height="572" /></a>
<h3>Remove Missing Folders</h3>
Go back to the Code Snippets Manager and click the *Remove* button for all the missing folders. When you are done, it should look like this.
<a href="/images/uploads/2016/01/Sql08.png"><img class="alignnone size-full wp-image-737" src="/images/uploads/2016/01/Sql08.png" alt="Sql08" height="459" /></a>
<h3></h3>
<h3>Insert new Template</h3>
Finally, the payoff. Press <b>^KX</b> to Insert a tempalte. It should be clean like this:
<a href="/images/uploads/2016/01/Sql091.png"><img class="alignnone size-full wp-image-740" src="/images/uploads/2016/01/Sql091.png" alt="Sql09" height="402" /></a>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Markdown site (Wiki) to share project information</title>
      <link href="https://scottbrooks.info/posts/markdown-site-(wiki)-to-share-project-information/"/>
      <updated>2016-01-28T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/markdown-site-(wiki)-to-share-project-information/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
I needed an easy way to share information with other members of our team.  Some people were using OneNote, which is a great product, but sharing things can be a little clunky.  Our organization was working towards an enterprise solution for this problem, but I needed something right now for what I was working on.
I chose Markdown since I figured it would be easy to convert it to whatever solution the company decided.  This ended up being a very useful tool and provided a quick easy way to collaborate with other teammates.
<h2>Screens</h2>
<h3>Home Page</h3>
<a href="/images/uploads/2016/01/Wiki01.png"><img src="/images/uploads/2016/01/Wiki01.png" alt="Wiki01" height="1024" class="alignnone size-full wp-image-820" /></a>
<h3>Home Page - Markdown</h3>
Example markdown
<a href="/images/uploads/2016/01/Wiki02.png"><img src="/images/uploads/2016/01/Wiki02.png" alt="Wiki02" height="1027" class="alignnone size-full wp-image-821" /></a>
<h3>Checklists</h3>
Added ability to easily create lists.
<a href="/images/uploads/2016/01/Wiki03.png"><img src="/images/uploads/2016/01/Wiki03.png" alt="Wiki03" height="1023" class="alignnone size-full wp-image-822" /></a>
<h3>Tools used</h3>
<a href="/images/uploads/2016/01/Wiki04.png"><img src="/images/uploads/2016/01/Wiki04.png" alt="Wiki04" height="1027" class="alignnone size-full wp-image-823" /></a>
<h3>Help pages</h3>
To make it easy for others to edit pages, I created a simple help page with examples.
<a href="/images/uploads/2016/01/Wiki06.png"><img src="/images/uploads/2016/01/Wiki06.png" alt="Wiki06" height="1029" class="alignnone size-full wp-image-825" /></a>
<p><a href="/images/uploads/2016/01/Wiki05.png"><img src="/images/uploads/2016/01/Wiki05.png" alt="Wiki05" height="1026" class="alignnone size-full wp-image-824" /></a></p>
<h3>Read Database Settings</h3>
Since this is custom code, it was easy to add the ability to execute Sql statements when the user clicked a link.  In this example, I display the AppSettings by environment on the page.
<a href="/images/uploads/2016/01/Wiki07.png"><img src="/images/uploads/2016/01/Wiki07.png" alt="Wiki07" height="1027" class="alignnone size-full wp-image-826" /></a>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Integrate Database Migrations with Octopus Deploy</title>
      <link href="https://scottbrooks.info/posts/integrate-database-migrations-with-octopus-deploy/"/>
      <updated>2016-01-29T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/integrate-database-migrations-with-octopus-deploy/</id>
      <content type="html">
        <![CDATA[
      <h2>Summary</h2>
<p>Prior to Octopus, we had a proprietary way of deploying database changes.  Moving to <a href="https://octopus.com/" target="_blank" rel="noopener noreferrer">Octopus Deploy</a> forced us to find an alternative approach.  I had used <a href="https://github.com/schambers/fluentmigrator" target="_blank" rel="noopener noreferrer">Fluent Migrations</a> on an earlier project and I was excited about using it again.</p>
<h2>Benefits</h2>
<ul>
<li>Database source is treated as first class citizen and remains in the project.</li>
<li>Full text search can easily be done to find each time a field or sproc was changed.</li>
<li>Stored Procedures can be easily compared between migrations.</li>
<li>Version History maintained in database (see screen capture below) showing exactly when each migration was applied (and Version, ITMS, PBI, or other info).</li>
<li>Multi-user Two people can be working on their own db changes at the same time, each in their own migration. (The normal rules that both of them should not be changing the exact same objects still apply).</li>
<li>Ability to apply multiple "iterations" during a sprint (instead of waiting until the end)</li>
<li>All Database changes are tracked together.</li> 
<li>This approach eliminates the need to push a db change using email or some other method. In our current environment, we are forced to do that. For a simple change, it just doesn't make sense to create a new branch of managed code, jump through the Fortify hoops and everything else that is involved in rolling a simple change, force all users out of the system, etc... With this approach, adding a simple migration should take less than 30 minutes and it is done.</li>
<li>Gives visibility to all changes, which can greatly assist troubleshooting down the road.</li>
<li>Fortify Tango not required. The current Implementation folder is not part of the Fortify process and this shouldn't need to be either. There is no User component to this. This is not an application that is being installed for the user.</li>
<li>Database changes are not tightly coupled to managed code changes. Some people would list this as a Con, but having them "separate" has some benefits.</li>
<li>Best Practices (and possibly worst) are more easily followed for future devs on how to script an extended property or foreign key. The prior scripts are part of the project and can be searched and copied.</li>
<li>Developer is not forced to use migrations. The can still wait until right before the deploy, let a tool do the compares, generate the scripts, and then just copy them into the folder.</li>
<li>Deploy and rollback can easily be ran multiple times during development, EXACTLY as they will be ran during deployment.</li>
<li>Visual Studio 2015 new project - added by default</li>
<h2>Screens</h2>
<h3>Migration history in DB</h3>
<a href="/images/uploads/2016/01/Migration01.png"><img src="/images/uploads/2016/01/Migration01.png" alt="Migration01" height="399" class="alignnone size-full wp-image-842" /></a>
<h3>PowerShell script</h3>
<a href="/images/uploads/2016/01/Migrate02.png"><img src="/images/uploads/2016/01/Migrate02.png" alt="Migrate02" height="855" class="alignnone size-full wp-image-843" /></a>
<h2>Technology / Tools</h2>
<ul>
   <li><a href="https://octopus.com/" target="_blank" rel="noopener noreferrer">Octopus Deploy</a></li>
   <li><a href="https://github.com/schambers/fluentmigrator" target="_blank" rel="noopener noreferrer">Fluent Migrations</a></li>
   <li>PowerShell</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Microservices - my recent journey</title>
      <link href="https://scottbrooks.info/posts/microservices-my-recent-journey/"/>
      <updated>2016-02-15T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/microservices-my-recent-journey/</id>
      <content type="html">
        <![CDATA[
      <h1>Summary</h1>
<p>I have been watching videos and reading for the past couple months about Microservices. It is very timely, since I have been doing maintenance work on a monolith project, parts of which are more than 10 years old.</p>
<p>I wanted to create this page to have a place to reference some of the material I have found along the way.</p>
<h2>Mastering Chaos - A Netflix Guide to Microservices</h2>
It is always good to gain more insight into how things work at Netflix. In this video, Josh Evans covers this history of the early Netflix to today, and some of where it is headed.
https://youtu.be/CZ3wIuvmHeM
<h2>Little Boy Dominoes Topple Out of the Tent</h2>
Ok, this one is not so much about Microservices as it is just 24 seconds of funny. I saw this video as part of a talk where the lady was making the point that we don't realize the impact of design decisions unless we measure (or in this case watch) our product (API) being used. The mom laughing as she films it and the final boy make me laugh every time.
https://youtu.be/WyKW4ERhvxo
<a href="#">top</a>
<h2>Spotify Engineering Culture part 1</h2>
https://youtu.be/4GK1NDTWbkY
<a href="#">top</a>
<h2>Spotify Engineering Culture part 2</h2>
https://youtu.be/Ddpm4IObKeA
<a href="#">top</a>
<h2>Challenges in Implementing MicroServices</h2>
This video is from Fred George. I have it set to begin where he starts talking about rivers, streams, and ponds. From there, he blends into the car rental application which is a great example of an incremental application.
https://youtu.be/yPf5MfOZPY0?t=21m5s
Here is an expanded and more recent version of that same talk.
https://youtu.be/YRyXyn-DVjU
<a href="#">top</a>
<h2>Microservices • Martin Fowler</h2>
https://youtu.be/wgdBVIX9ifA
<a href="#">top</a>
<h2>A Day in the Life of a Netflix Engineer</h2>
https://youtu.be/-mL3zT1iIKw
<a href="#">top</a>
<h2>Microservices @ Spotify</h2>
Kevin Goldsmith going into more detail about how Spotify uses Microservices.
https://youtu.be/7LGPeBgNFuU
<a href="#">top</a>
<h2>Microsoft Azure Service Bus Brokered Messaging Deep Dive</h2>
Alan Smith - the same guy that does the PluralSight course on the same subject. With the speed of Azure, this may be a little dated, but I think the majority of it still applies.
https://youtu.be/9tyZKkJD0tQ
<a href="#">top</a>
<h2>Agile is Dead • Pragmatic Dave Thomas</h2>
https://youtu.be/a-BOSpxYJ9M
<a href="#">top</a>
<h2>Agile Product Ownership in a Nutshell</h2>
This is the same guy that did the Spotify culture videos.
https://youtu.be/502ILHjX9EE
<a href="#">top</a>
<h2>"The Eight Fallacies of Distributed Computing"</h2>
by Stephen Asbury. I liked this video where he is trying to shed some light on the issues of distributed computing. He is not saying, "Don't do it". Instead, I think he is just helping to remind us of the things to be aware of and code for.
https://youtu.be/JG2ESDGwHHY
<a href="#">top</a>
<h2>Wunderlist - Chat Fowler</h2>
From Homogeneous Monolith to Radically Heterogeneous Microservices Architecture
https://youtu.be/sAsRtZEGMMQ
<a href="#">top</a>
<h2>Applying the Saga Pattern</h2>
Caitie McCaffrey
https://youtu.be/xDuwrtwYHu8
<a href="#">top</a>
<h2>Patterns and Practices for Real-World Event-Driven Microservices</h2>
This lady is from Jet (that wants to compete with Amazon) and they are a big Azure and F# shop
https://vimeo.com/144692770
<a href="#">top</a>
<h2>Juval Lowy on Microservices</h2>
This is a very smart guy talking about Microservices and how to think of them. The volumne is a little low, but it is definately worth watching.
https://channel9.msdn.com/Blogs/raw-tech/Juval-Lowy-On-Microservices
<a href="#">top</a>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Idea wall</title>
      <link href="https://scottbrooks.info/posts/idea-wall/"/>
      <updated>2016-04-03T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/idea-wall/</id>
      <content type="html">
        <![CDATA[
      <p><a href="/images/uploads/2016/04/160403_Insulation.jpg"><img class="alignright size-medium wp-image-1006" src="/images/uploads/2016/04/160403_Insulation-300x300.jpg" alt="160403_Insulation" height="300" border="0" /></a></p>
<p>I have been a fan of cards on a wall for years. I buy the 1/2&quot; 4'x8' sheets of <a href="http://commercial.owenscorning.com/products/foam/foamular-sheathing/" target="_blank" rel="noopener noreferrer">insulation</a>. In the past, they were about $8, but the latest at <a href="http://www.homedepot.com/p/Owens-Corning-FOAMULAR-1-2-in-x-4-ft-x-8-ft-R-3-Squared-Edge-Insulating-Sheathing-36L/100320356" target="_blank" rel="noopener noreferrer">Home Depot</a> is more like 14.</p>
<p>In my current setup, I added a few curtain rails along the top of my whiteboard. Then, I cut the insulation boards down to a smaller size and use them for &quot;cork boards&quot;.</p>
<p>Here is a recent example of one of my boards.
<img class="alignnone size-medium wp-image-1015" src="/images/uploads/2016/04/IMG_1755-300x225.jpg" alt="IMG_1755" height="225" /></p>
<h2>Luke - What is this board for?</h2>
He came in this morning and asked why I had all the cards on the board. I figured it would be easier to show him than to explain it. I grabbed a clean board and we started "brainstorming" the treehouse he wants to build. He likes things a little more visual, so I printed some pics and attached them to the cards.
<a href="/images/uploads/2016/04/IMG_1757.jpg"><img class="alignnone size-large wp-image-1016" src="/images/uploads/2016/04/IMG_1757-1024x768.jpg" alt="IMG_1757" height="469" /></a>
<p>When I first put the clean board up and told him it was for the treehouse, he was pretty skeptical. I believe he was thinking were going to actually start building the treehouse right now. However, once we got a few cards on the board, I think he got the idea. He is a fan now (although, I am sure he rather just have the treehouse)
<a href="/images/uploads/2016/04/IMG_1758.jpg"><img class="alignnone size-large wp-image-1017" src="/images/uploads/2016/04/IMG_1758-1024x768.jpg" alt="IMG_1758" height="469" /></a></p>
<p>Here is an example of the top of the board where you can see the curtain rods. And, I know you are wondering... Yes, I did <i>wrap the edge in a little Duct tape</i> just to give it that <b>professional look.</b> :-)
<a href="/images/uploads/2016/04/IMG_1761.jpg"><img class="alignnone size-large wp-image-1014" src="/images/uploads/2016/04/IMG_1761-1024x768.jpg" alt="IMG_1761" height="469" /></a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Html PlaceHolder Attribute done right</title>
      <link href="https://scottbrooks.info/posts/html-placeholder-attribute-done-right/"/>
      <updated>2016-05-28T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/html-placeholder-attribute-done-right/</id>
      <content type="html">
        <![CDATA[
      <p>While a pet peeve is described as a &quot;minor annoyance&quot;, this issue has always been a little more than that for me.   I don't know how many times I see a textbox on a form somewhere and when I click in the textbox, the label disappears.   I get distracted for a moment, and when I look back at the screen, I can't remember what I was expected to be entering.  It happens a lot with Logins.   Once I start to log in, I wonder, did they want a username or an email?</p>
<p>So, when I was signing up with <a href="https://www.grammarly.com/" target="_blank" rel="noopener noreferrer">Grammarly</a> (I know, from my posts, you wouldn't guess I would need their service :-) ), I was pleased to find out they are doing inputs &quot;right&quot;.</p>
<p>When I clicked into the input, the caption animated down to a smaller version so I could still see which input field I was in.</p>
<p>It is odd to create a blog post for such a minor thing, but I was just so happy to see somebody doing it right that I had to post it somewhere.</p>
<p><a href="/images/uploads/2016/05/160528_PlaceholderMinimized.jpg"><img src="/images/uploads/2016/05/160528_PlaceholderMinimized.jpg" alt="160528_PlaceholderMinimized" height="766" class="alignnone size-full wp-image-1054" /></a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Weekend Brain Dump</title>
      <link href="https://scottbrooks.info/posts/weekend-brain-dump/"/>
      <updated>2016-07-02T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/weekend-brain-dump/</id>
      <content type="html">
        <![CDATA[
      <p>It is no secret that I am a fan of checklists.   Tools like <a href="http://trello.com/" target="_blank" rel="noopener noreferrer">Trello</a> are awesome.  I also use <a href="http://www.nirvanahq.com/" target="_blank" rel="noopener noreferrer">NirvanaHq</a> for my <a href="http://gettingthingsdone.com/" target="_blank" rel="noopener noreferrer">GTD</a> type lists (Thanks <a href="http://gettingthingsdone.com/meet-david/" target="_blank" rel="noopener noreferrer">David Allen</a>).  One of the few books that I am reading right now,
<a href="https://www.amazon.com/Checklist-Manifesto-How-Things-Right/dp/0312430000" target="_blank" rel="noopener noreferrer">The Checklist Manifesto</a> is about how important checklists can be.</p>
<p>However, sometimes I know I have things on those lists, but they are still swimming around in my head.   I have the mind like water that David refers to, but it is not the calm still water.  Instead, it feels like the rapids.</p>
<p>When I reach that point, I find it helpful to grab a pile of notepads and just start dumping things out of my head.   I don't like using just one notepad and flipping pages, since things seem to get hidden that way.   Instead, I dedicate a notebook for each &quot;area&quot; that I am &quot;dumping&quot;.   I can easily lay them out and see everything.</p>
<p>Here is a picture from this morning.   I am trying to get into the habit of keeping an index card with the 2 or 3 things that I hope to get done in a day.   For this day, it was so overwhelming, I just picked one.   BTW, the breakfast was great and the boys (<a href="http://growingupluke.com/2016/05/28/the-boys-at-the-drive-in/" target="_blank" rel="noopener noreferrer">these boys</a>) had a good time.<a href="/images/uploads/2016/07/160703_WeekendBrainDump.jpg"><img src="/images/uploads/2016/07/160703_WeekendBrainDump.jpg" alt="160703_WeekendBrainDump" height="514" class="alignnone size-full wp-image-1063" /></a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>With friends like these...</title>
      <link href="https://scottbrooks.info/posts/with-friends-like-these.../"/>
      <updated>2016-07-23T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/with-friends-like-these.../</id>
      <content type="html">
        <![CDATA[
      <p>Here is a clip that shows the kind of friends you want to have when you are in the &quot;flow&quot;.</p>
<p><a href="https://1drv.ms/v/s!ApcCxSkX75koke8sO_i-PymuQb5tsQ"><img src="/images/uploads/2016/07/160723_Friends.jpg" alt="160723_Friends" height="478" class="alignnone size-full wp-image-1075" /></a></p>
<p>(The sound is off, but you will get the idea)</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Dropping files in Electron (with Angular)</title>
      <link href="https://scottbrooks.info/posts/dropping-files-in-electron-(with-angular)/"/>
      <updated>2017-05-21T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/dropping-files-in-electron-(with-angular)/</id>
      <content type="html">
        <![CDATA[
      <p>I started with a clone this project.
<a href="https://github.com/maximegris/angular-electron">https://github.com/maximegris/angular-electron</a></p>
<p>My goal was to find an easy way for a user to drop files into an Electron app.</p>
<p>Electron makes this pretty easy by providing the <a href="https://electron.atom.io/docs/api/file-object/"><code>File</code> Object</a>.   However, that code was not working for me, since I was working in an Angular app.</p>
<p><a href="http://stackoverflow.com/questions/35570746/angular2-including-thirdparty-js-scripts-in-component/35570783#35570783">This Post</a> on StackOverflow helped me solve the issue.   (This <a href="http://stackoverflow.com/users/217408/g%C3%BCnter-z%C3%B6chbauer">Gunter</a> guy is awesome!)</p>
<p><img src="https://brspublic.blob.core.windows.net/blog/2017/05/20/170520_StackOverflowAddingScriptToAngular.png" alt="StackOverflow post"></p>
<p>The only small hurdle was finding the right place to put the dropscript.js.  This is what I ended up with.
<img src="https://brspublic.blob.core.windows.net/blog/2017/05/20/170520_DropScript.png" alt=""></p>
<h2>Links</h2>
<ul>
<li>This post <a href="https://auth0.com/blog/create-a-desktop-app-with-angular-2-and-electron/">Create a Desktop App with Angular 2 and Electron</a> looked pretty interesting, but I think it was targeted to a mac, and I was on my Windows box. (<a href="https://github.com/auth0-blog/angular2-electron">github</a>)</li>
</ul>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Publishing ServiceBus message from Node (Electron)</title>
      <link href="https://scottbrooks.info/posts/publishing-servicebus-message-from-node-(electron)/"/>
      <updated>2017-05-21T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/publishing-servicebus-message-from-node-(electron)/</id>
      <content type="html">
        <![CDATA[
      <p>This is a handy little module that wraps up the Azure ServiceBus code in a tight little package.   I don't claim any of it as original work, since I have pulled from more sources than I can remember.</p>
<p>It is a work in progress, but I wanted to document what I have so far.</p>
<h2>Code for azHelper.js</h2>
<pre><code>&quot;use strict&quot;;
<p>var azure = require(&quot;azure-sb&quot;);
var sbConnect = &quot;Endpoint VALID CONNECTION STRING HERE...&quot;;
var sbService = azure.createServiceBusService(sbConnect);</p>
<p>function publishMessage(messageLabel, message, category) {</p>
<p>if (!category) {
category = 1000;
}
console.log(&quot;Publishing message&quot;, messageLabel);</p>
<p>var sbMessage = {
body: JSON.stringify(message),
brokerProperties: {
Label: messageLabel
},<br>
customProperties: {
Category: category
}
};</p>
<p>sbService.sendTopicMessage(&quot;rapids&quot;, sbMessage, sbCallback);
}</p>
<p>var sbCallback = function(err) {
if (err) {
console.log(&quot;ERROR publishing message&quot;, err);
} else {
console.log(&quot;message published&quot;);
}
};</p>
<p>module.exports = {
publish: publishMessage
};
</code></pre></p>
<h2>And the calling code...</h2>
<pre><code class="language-javascript">var azHelper = require(&quot;./azhelper.js&quot;);
<p>var contentFoundMessage = {
fileName: fileName,
baseName: baseName
};</p>
<p>azHelper.publish(&quot;ContentFound&quot;, contentFoundMessage, 1100);
</code></pre></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Unique object names for S3</title>
      <link href="https://scottbrooks.info/posts/unique-object-names-for-s3/"/>
      <updated>2017-05-21T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/unique-object-names-for-s3/</id>
      <content type="html">
        <![CDATA[
      <p>When creating buckets or objects in S3, I wanted an easy way to create a unique name for each object.</p>
<p>The names to be as short and human readable as possible.  (Guids have their place, but I don't want to look at them).</p>
<p>The Aws <a href="http://docs.aws.amazon.com/AmazonS3/latest/dev/BucketRestrictions.html">Rules for Bucket Naming</a> lists the following guidelines.</p>
<ul>
<li>Bucket names must be at least 3 and no more than 63 characters long.</li>
<li>Bucket names must be a series of one or more labels. Adjacent labels are separated by a single period (.). Bucket names can contain lowercase letters, numbers, and hyphens. Each label must start and end with a lowercase letter or a number.</li>
<li>Bucket names must not be formatted as an IP address (e.g., 192.168.5.4).</li>
<li>When using virtual hosted–style buckets with SSL, the SSL wildcard certificate only matches buckets that do not contain periods. To work around this, use HTTP or write your own certificate verification logic. We recommend that you do not use periods (&quot;.&quot;) in bucket name</li>
</ul>
<h2>NPM shortid</h2>
<p>In the past, I had settled on using the NPM package <a href="https://www.npmjs.com/package/shortid">shortid</a>, but it did not seem to be returning the first character randomly.  Based on my recent listen to the Pluralsight course <a href="https://www.pluralsight.com/courses/file-storage-aws-s3-glacier-developer-deep-dive">File Storage Using AWS S3 and Glacier: Developer Deep Dive</a>, I want my names to be random, so Amazon can better spread the load out on the hardware.</p>
<h2>Custom Id Generation</h2>
<p>The bulk of this code comes directly from  <a href="http://fiznool.com/blog/2014/11/16/short-id-generation-in-javascript/">Tom Spencer's post - Short ID Generation in JavaScript</a>.  His use case was a little different, but you should read it for a more generic approach.   I wanted something that took the date into consideration, so I hacked his approach.</p>
<h2>Finally, some code</h2>
<p>Here is my s3helper.js module</p>
<pre><code>&quot;use strict&quot;;
<p>module.exports = (function() {
const moment = require(&quot;moment&quot;);</p>
<p>// Just to ensure we don't start a name with a number, we
// use alpha only for the prefix.
const LIST_ALPHA = &quot;abdegjkmnpqrvwxyz&quot;;
const LIST_ALL = &quot;23456789&quot; + LIST_ALPHA;</p>
<p>const ALL_LENGTH = LIST_ALL.length;
const BUCKET_ID_LENGTH = 8;
const OBJECT_ID_LENGTH = 3;</p>
<p>var getPrefix = function() {
var retVal = LIST_ALPHA.charAt(
Math.floor(Math.random() * LIST_ALPHA.length)
);
return retVal;
};</p>
<p>function getbucketName() {
var retVal = getPrefix();
for (var iLoop = 0; iLoop &lt; BUCKET_ID_LENGTH; iLoop++) {
retVal += LIST_ALL.charAt(Math.floor(Math.random() * ALL_LENGTH));
}
return retVal;
}</p>
<p>function getobjectName() {
var retVal = getPrefix();
for (var iLoop = 0; iLoop &lt; OBJECT_ID_LENGTH; iLoop++) {
retVal += LIST_ALL.charAt(Math.floor(Math.random() * ALL_LENGTH));
}</p>
<p>retVal += &quot;-&quot; + moment().diff(&quot;2014-08-26T08:01&quot;, &quot;seconds&quot;);
return retVal;
}</p>
<p>return {
bucketName: getbucketName,
objectName: getobjectName
};
})();
</code></pre></p>
<p>Using the code is pretty easy.</p>
<pre><code>console.log(&quot;s3&quot;, s3Helper.bucketName(), s3Helper.objectName());
</code></pre>
<p>Which gives me this output in the console.</p>
<p><img src="https://brspublic.blob.core.windows.net/blog/2017/05/20/170520_s3names.png" alt=""></p>
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>2017 Videos, Links, whatever</title>
      <link href="https://scottbrooks.info/posts/2017-videos-links-whatever/"/>
      <updated>2017-06-01T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/2017-videos-links-whatever/</id>
      <content type="html">
        <![CDATA[
      <h1>My Ramblings...</h1>
[toc]
<h2>Guy Kawasaki - The Lessons of Steve Jobs - Lean Startup Week 2016</h2>
Guy is a great speaker, full of energy.  This is a fun quick video to watch where he does a top 10 from Steve.   Not to give it away, but I liked the Steve quote, "Some things need to be believed to be seen".
https://youtu.be/Ou_UpB4Cijk
<h2>Jake Archibald - Instant Loading: Building offline-first Progressive Web Apps - Google I/O 2016</h2>
This is a very fun to watch video from a very skilled presenter.   Ward Bell suggested this on a podcast I recently listened to.   Ward pointed out to listen for the "We can ship that" multiple times in the talk which is funny.   It gives a thorough explanation of ServiceWorkers and how to progressively update an app to use them.
https://youtu.be/cmGr0RszHc8
<h2>Jason Fried - Why 40 Hours is Enough: Lessons from Basecamp - Lean Startup 2016</h2>
Excellent talk from a fellow who has been "doing it" for the past 17 years.   I have always NOT been a fan of email for project related communications.  Now that I hear this talk, I wonder if some of my original problems with that type of communication came from reading his book (<a href="https://gettingreal.37signals.com/" target="_blank" rel="noopener noreferrer">Getting Real</a>,  which I read back in 2006).
https://youtu.be/r_cc-JrTe38
<h2>Sam Newman - Feature Branches and Toggles in a Post-GitHub World</h2>
This guy literally wrote the book on Building Microservices.  In this talk he gives a nice history of version control and git.  He shows the challenges with feature branching and suggests an alternative "Branch-by abstraction".   This is a major part of using feature toggles.  In relation to flags, he suggests using a flag in as few places as possible and remove the flag when it is no longer needed.  Another thought is to treat every check-in as a release candidate.  He does a great job talking about the problem of having too many changes in a release, which causes it to take longer, which increases the potential for problems (both deployment and one change breaking something else).   He also talks about trunk based development.  For the right teams on the right kinds of projects, this seems like the sweet spot to me.
https://youtu.be/lqRQYEHAtpk
<h2>Simon Brown - Modular Monoliths</h2>
He tries to make documentation match the software it is intended to describe.  He leans a little more towards continuing the monoliths than I prefer, but I do like the idea of simple architecture designs that actually describe the software.
https://youtu.be/kbKxmEeuvc4
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>2018 Videos, Links, &amp; Whatever...</title>
      <link href="https://scottbrooks.info/posts/2018-videos-links-and-whatever.../"/>
      <updated>2018-03-12T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/2018-videos-links-and-whatever.../</id>
      <content type="html">
        <![CDATA[
      <h1>My Ramblings...</h1>
[toc]
<h2>Mac Powell - Super Dad!</h2>
It has been months since I shared a video, but I just had to share this one.  It is cool that Mac brings his daughter up on stage to fulfill one of her dreams.  However, the part I like is when he realizes that he wants to be sure not to embarrass her with a song she may not know the words to (especially with her nervousness of being on stage in front of this many people).  
So, he enlists the crowd to sing along.  It becomes like any other worship session where the leader is saying the words before each verse.  I believe Mac is doing this not only for the audience, but also to make sure his daughter feels safe in the process and doesn't get tripped up.  
I would guess she has sang this song a thousand times in her life, but everything is different on a stage.  Mac knows that and is protecting her.  It is a valuable lesson for any dad.
https://youtu.be/XbXkjDK3yqg
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>GraphQL - From the beginning</title>
      <link href="https://scottbrooks.info/posts/graphql-from-the-beginning/"/>
      <updated>2018-07-13T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/graphql-from-the-beginning/</id>
      <content type="html">
        <![CDATA[
      <p>I have seen a couple videos on GraphQL in the past, but it has been some time.   So, in one sense, I am starting fresh.</p>
<p>Here is the first video I watched this evening.  It is a very good overview of GraphQL with a nice intro about some of the limitations of REST.
https://youtu.be/KoSSlxjTdXk</p>
<p>The video included a link to <a href="https://graphql.org/swapi-graphql/" target="_blank" rel="noopener noreferrer">a site to play around with things</a> at this url:<br>
Here is a good query to start with</p>
<pre>{
  allFilms {
    films {
      id,
      title,
      director,
      openingCrawl
    }
  }
}</pre>
<p>More to come...</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>2019 Videos, Links, &amp; Whatever...</title>
      <link href="https://scottbrooks.info/posts/2019-videos-links-and-whatever.../"/>
      <updated>2019-06-19T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/2019-videos-links-and-whatever.../</id>
      <content type="html">
        <![CDATA[
      <h2>Wright Brothers vs <a href="https://en.wikipedia.org/wiki/Samuel_Pierpont_Langley" target="_blank" rel="noopener noreferrer">Samuel Pierpont Langley</a></h2>
<p>
I am listening to <a href="https://en.wikipedia.org/wiki/The_Wright_Brothers_(book)" target="_blank" rel="noopener noreferrer">The Wright Brothers</a> audio book by <a href="https://en.wikipedia.org/wiki/David_McCullough" target="_blank" rel="noopener noreferrer">David McCullough</a>. I have really been enjoying it. You may remember his voice from <a href="https://en.wikipedia.org/wiki/The_Civil_War_(miniseries)" target="blank" rel="noopener noreferrer"><i>The Civil War</i></a> series by Ken Burns.
Listening to the book reminded me of a talk I heard earlier by <a href="https://en.wikipedia.org/wiki/Simon_Sinek" target="_blank" rel="noopener noreferrer">Simon Sinek</a> where he compared the resources of the Wright brothers to Samuel Langley. In the book, McCullough noted that Langley had over 50K and a dream team for his work. Meanwhile, the Wright brothers spent less than $1000 of their own money on the entire venture.</p>
<div class="youtube-embed">
      <a href="https://www.youtube.com/watch?v=tGfE_SBr6eA" target="_blank" rel="noopener">
        <img src="https://img.youtube.com/vi/tGfE_SBr6eA/hqdefault.jpg" alt="YouTube video thumbnail" loading="lazy">
        <span class="play-button">▶</span>
      </a>
    </div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Estimates</title>
      <link href="https://scottbrooks.info/posts/estimates/"/>
      <updated>2019-10-13T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/estimates/</id>
      <content type="html">
        <![CDATA[
      
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>When management thinks 100 junior devs can do the work of 3 senior devs</title>
      <link href="https://scottbrooks.info/posts/when-management-thinks-100-junior-devs-can-do-the-work-of-3-senior-devs/"/>
      <updated>2019-10-29T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/when-management-thinks-100-junior-devs-can-do-the-work-of-3-senior-devs/</id>
      <content type="html">
        <![CDATA[
      
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>NotebookLM Created Audio Magic for Our Tagaytay Trip</title>
      <link href="https://scottbrooks.info/posts/notebooklm-created-audio-magic-for-our-tagaytay-trip/"/>
      <updated>2025-06-15T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/notebooklm-created-audio-magic-for-our-tagaytay-trip/</id>
      <content type="html">
        <![CDATA[
      <p>I recently discovered Google's NotebookLM. Press the Play button below to hear the podcast. This is all &quot;made up&quot; AI voices. I am just blown away that it can produce this.</p>
<audio controls style="width: 100%; max-width: 600px; margin: 1rem 0;">
  <source src="/images/NotebookLm-Tagatay.mp3" type="audio/mpeg">
  Your browser does not support the audio element.
</audio>
<p>We were planning a trip to the Philippines and we were looking for some things we could do while we were there. I don't remember the exact prompt I used, but I said we were going to be staying about an hour south of Manila to spend time with our kids and new granddaughter. Using its Discover sources feature, it finds 10 sites that it thinks would be helpful. Then I just do that a few more times until it has 40-50 sources. That is it. I click generate to get the audio, and it does everything else.</p>
<h2>What is NotebookLM?</h2>
<p>For those unfamiliar, <a href="https://notebooklm.google.com/" target="_blank" rel="noopener noreferrer">NotebookLM</a> is Google's AI-powered note-taking and research assistant. But it's evolved far beyond just taking notes. One of its most impressive features is the ability to generate podcast-style audio conversations from your sources.</p>
<h2>The Magic of Audio Generation</h2>
<p>Here's what makes this feature so special: NotebookLM creates a natural conversation between two AI hosts who discuss your content in an engaging, insightful way. They add context, ask each other questions, and highlight interesting points - all based on your original material.</p>
<h2>The Result</h2>
<p>What came back was a 10-minute audio conversation that felt like two travel enthusiasts discussing our trip.</p>
<h2>How to Try It Yourself</h2>
<p>If you want to create your own audio from NotebookLM:</p>
<ol>
<li>Go to <a href="https://notebooklm.google.com/">notebooklm.google.com</a></li>
<li>Create a new notebook and upload your documents (supports PDFs, Google Docs, text files, URLs, etc.)</li>
<li>Once your content is loaded, look for the &quot;Audio Overview&quot; feature</li>
<li>Click to generate, and in a few minutes, you'll have your custom podcast</li>
</ol>
<p>The generated audio is free, and you can regenerate it a few times a day if you add more content or want a different take.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>What is the best way to provide content to Copilot</title>
      <link href="https://scottbrooks.info/posts/what-is-the-best-way-to-provide-content-to-copilot/"/>
      <updated>2026-02-15T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/what-is-the-best-way-to-provide-content-to-copilot/</id>
      <content type="html">
        <![CDATA[
      <div class="question">
<p>At my work, we have a knowledge base that is primarily a website. We call it AcmeKnowledgebase. Most of the content is stored in a SQL Server database in markdown. There are screenshots (png, jpg) and videos (mp4). It is organized as a page full of books. The user clicks the book they are interested in, and from there they go to the next page that displays all the chapters for the book along the left side of the screen and the content of each chapter displayed on the right. The content for the first chapter is displayed as soon as the page is loaded.</p>
<p>I would like to create a Microsoft Teams app to allow the user to have a conversation with this data. What is the best way to make this content available to an LLM? We use Microsoft Copilot at work and that would be something we prefer to use. One thing that we do not really want to do is ingest all of the AcmeKnowledgebase content into the Microsoft Graph.</p>
<p>Can you suggest how I should make this content available to Copilot running in a Teams application?</p>
</div>
<div class="answer">
<p>Given that your AcmeKnowledgebase content lives in SQL Server and you specifically don't want to ingest everything into Microsoft Graph, you have a solid path forward. Here's the approach I'd recommend.</p>
<h2>Build a REST API over your AcmeKnowledgebase data, then expose it as an API Plugin for a Declarative Agent</h2>
<p>The core idea is straightforward: you create a thin REST API that sits in front of your SQL Server database, then register it with Microsoft Copilot as an <strong>API plugin</strong> attached to a <strong>declarative agent</strong>. This lets Copilot call your API at query time rather than requiring you to push all your content into Graph or SharePoint.</p>
<p><strong>Step 1 — Build a AcmeKnowledgebase REST API.</strong> You likely already have some data access layer for your web app. You'd create (or extend) a set of API endpoints that Copilot can call to search and retrieve content. Think along the lines of <code>GET /books</code>, <code>GET /books/{id}/chapters</code>, <code>GET /chapters/{id}/content</code>, and critically a <code>GET /search?q=...</code> endpoint. Since your content is markdown in SQL Server, you'll want to add full-text search if you haven't already — SQL Server's built-in full-text indexing works fine here, or you could stand up Azure AI Search over that same data for better semantic retrieval. The search endpoint is the most important one, because that's how Copilot will find relevant content to ground its answers.</p>
<p><strong>Step 2 — Write an OpenAPI specification.</strong> This is what Microsoft's tooling uses to understand what your API can do. You describe each endpoint, its parameters, and — this is crucial — write clear, natural-language descriptions of what each operation does and when it should be called. Copilot's orchestrator uses these descriptions to decide which endpoints to invoke for a given user question. For example, your search endpoint description might say &quot;Search the AcmeKnowledgebase knowledge base for chapters and content matching the user's question about internal processes, tools, or procedures.&quot;</p>
<p><strong>Step 3 — Create a Declarative Agent with the API Plugin.</strong> Using either <strong>Copilot Studio</strong> or the <strong>Microsoft 365 Agents Toolkit</strong> (in VS Code), you create a declarative agent. This is where you define the agent's persona and instructions — something like &quot;You are the AcmeKnowledgebase assistant. Answer questions using only content from the AcmeKnowledgebase knowledge base. Always cite which book and chapter your answer comes from.&quot; You then attach your API as an API plugin by pointing it at your OpenAPI spec and configuring authentication. The agent gets published to Teams, and your users can chat with it there.</p>
<p><strong>Step 4 — Handle the media content.</strong> For screenshots and videos, your API can return URLs pointing to wherever those assets are hosted (your existing web server, Azure Blob Storage, etc.). Copilot can include these links in its responses, so users can click through to view images or videos. You won't get inline video playback in the chat, but you can give users a direct link.</p>
<h2>Why this approach works well for your situation</h2>
<p>Your content stays in SQL Server — no duplication into Graph or SharePoint. Copilot queries your API on demand, so updates to AcmeKnowledgebase are immediately reflected. You also maintain full control over what gets returned and how search works. The book/chapter structure you already have maps naturally to API endpoints, and the markdown content is already in a format that LLMs handle well.</p>
<h2>A few things to keep in mind</h2>
<p>Authentication is the main complexity. Your API needs to support OAuth or API key auth, and you'll configure this when registering the plugin. If your org already uses Azure AD (Entra ID), OAuth with delegated permissions is the cleanest approach.</p>
<p>Also, there's a practical limit on how much text Copilot can process per API call. You'll want your search endpoint to return focused, relevant chunks rather than entire chapters. If some of your chapters are very long, consider breaking the content into smaller sections or returning just the most relevant portions based on the search query. This is where Azure AI Search with semantic ranking could really help — it can pull back the most relevant passages rather than whole documents.</p>
<p>Finally, the Copilot Studio route gives you a no-code/low-code way to iterate on the agent's behavior, while the Agents Toolkit approach gives you more developer control. Given you have a dev team maintaining a web app with SQL Server, the Agents Toolkit path will probably feel more natural.</p>
<h3>Sources</h3>
<ul>
<li><a href="https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-api-plugins">API Plugins for Microsoft 365 Copilot</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/overview-declarative-agent">Declarative Agents Overview</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/declarative-agent-architecture">Declarative Agent Architecture</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-365-copilot/extensibility/agents-overview">Agents for Microsoft 365 Copilot</a></li>
<li><a href="https://learn.microsoft.com/en-us/microsoft-copilot-studio/agent-extend-action-rest-api">Extend Agents with REST API Tools (Preview)</a></li>
</ul>
</div>
<div class="question">
<p>Sorry, I should have been more clear. This site is entirely driven by an API (We actually did API first, so anything the client displays comes from the API). It is using OpenAPI and has a Swagger page. It is all written in .Net Core 6. So, we already have all that. One of things I was wondering is if we should consider adding MCP functionality to the existing API. I have seen videos where a developer adds them to an existing WebApi, and it seems pretty easy with the .Net MCP Toolkit (not sure of exact name).</p>
<p>My concern with either approach is how will Copilot query the endpoints? Relevant content could be &quot;buried&quot; in more than one book, and I don't want something to have to hit every endpoint for every request.</p>
<p>So, that may be the main point of my question. How can I provide this content to an LLM (is this a RAG question) without having to &quot;read&quot; it for every request?</p>
</div>
<div class="answer">
<p>You're asking exactly the right question, and yes, this is fundamentally a RAG question. Let me cut straight to the core issue.</p>
<h2>The real problem: retrieval, not plumbing</h2>
<p>You already have the API — that's great. But the challenge is this: if Copilot (or any LLM) has to figure out which books and chapters to call by browsing your endpoints, it's going to be slow, expensive, and unreliable. Imagine a user asking &quot;what's our policy on escalating customer complaints?&quot; — that answer might span content in a &quot;Customer Service&quot; book, a &quot;Management&quot; book, and an &quot;HR Policies&quot; book. No amount of clever endpoint descriptions in your OpenAPI spec will reliably guide Copilot to call the right three chapters out of potentially hundreds.</p>
<p>What you need is a <strong>search layer</strong> that sits between the LLM and your content. The LLM sends a query to one endpoint, and that endpoint returns the most relevant chunks of content regardless of which book or chapter they live in. That's RAG.</p>
<h2>What I'd recommend</h2>
<p><strong>Stand up a vector search index over your AcmeKnowledgebase content.</strong> Since you're in the Microsoft ecosystem, Azure AI Search is the natural choice — it supports both traditional full-text and vector (semantic) search, and it integrates well with the rest of your stack. The flow would look like this:</p>
<p>Your SQL Server content gets indexed into Azure AI Search. You'd write a relatively simple indexer that pulls your markdown content, splits it into reasonable chunks (say 500–1000 tokens each, preserving section boundaries where possible), generates embeddings for each chunk using an embedding model (Azure OpenAI's <code>text-embedding-ada-002</code> or similar), and stores them in the index along with metadata like book name, chapter title, and a URL back to that chapter in your web app.</p>
<p>Then you add <strong>one new endpoint</strong> to your existing API — something like <code>POST /search</code> that takes a natural language query, runs it against the Azure AI Search index, and returns the top N most relevant chunks with their metadata. This single endpoint is what the LLM calls. It doesn't need to know your book structure, it doesn't need to browse chapters — it just asks a question and gets back the relevant content.</p>
<p><strong>Keeping the index fresh</strong> is straightforward since you already control the data layer. You can either run the indexer on a schedule (every few minutes, hourly, whatever matches your update frequency) or trigger reindexing when content changes in your API's write operations. Azure AI Search has built-in indexer support for SQL Server as a data source, so this can be largely configuration rather than code.</p>
<h2>MCP vs. API Plugin — which to use</h2>
<p>On the MCP question: the .NET library you're thinking of is likely the <strong>Model Context Protocol C# SDK</strong> (there's an official one from Microsoft/Anthropic). Adding MCP server capabilities to your existing .NET API is indeed pretty straightforward. But here's the practical consideration right now — <strong>Microsoft 365 Copilot declarative agents support API plugins via OpenAPI specs</strong>, and they recently added MCP support as well (it's in preview). Since you already have a full OpenAPI/Swagger setup, the API plugin route is the path of least resistance for getting into Teams via Copilot.</p>
<p>That said, MCP is worth considering for a couple of reasons. It's becoming a broader standard beyond just Microsoft's ecosystem, so if you ever want to connect your AcmeKnowledgebase to other AI tools (Claude, VS Code Copilot, etc.), you'd have that covered. And the MCP &quot;tools&quot; model maps nicely to what you'd be building — essentially a <code>search_AcmeKnowledgebase</code> tool that any MCP-compatible client can call. You could realistically support both: your existing OpenAPI spec for the Copilot declarative agent, and an MCP server endpoint for broader compatibility. They'd both call the same search index underneath.</p>
<h2>The architecture in summary</h2>
<p>The key insight is that you're adding one new capability — semantic search — not rearchitecting anything. Your existing API, your SQL Server database, your web app all stay as they are. You add an Azure AI Search index that mirrors your content, one new search endpoint on your API, and a declarative agent in Teams that's instructed to use that search endpoint. A user asks a question in Teams, Copilot calls your search endpoint, gets back the three or four most relevant chunks of content across all books, and uses those to generate an answer with citations.</p>
<p>The indexing is the only part that might feel new if you haven't worked with vector search before, but Azure AI Search handles most of the complexity — especially since it can pull directly from SQL Server.</p>
</div>
<div class="question">
<p>I apologize again for not being more clear.   When we started this application (2016 I think), we wanted an easy way to search content.  So, every time a section of content changes, we preserve the prior content (event sourcing type pattern) and add a new record with the user's change/addition.   When we add that new record, the &quot;Sync&quot; column is null.  Then, about every 5 minutes, a background job looks for all the null values and syncs those records to Azure Search (It has changed names a few times since we started using it, but underneath, it still works about the same).</p>
<p>I just wanted to stress that we already have all of the content in Azure Search.</p>
</div>
<div class="answer">
<p>Oh, that changes things significantly — you're much further along than I was assuming. If you already have the content indexed in Azure AI Search, then the RAG infrastructure is already in place. The remaining question is really just about the &quot;last mile&quot;: how do you connect Copilot in Teams to that search index?</p>
<p>You have two clean options, and honestly they aren't mutually exclusive.</p>
<p><strong>Option 1: Add a search endpoint to your existing API and register it as an API plugin.</strong> You'd add a single endpoint to your .NET API that takes a query string, hits your Azure AI Search index, and returns the top relevant chunks with metadata. Then you create a declarative agent in Teams that uses your OpenAPI spec (just pointing at that one endpoint, not your whole API). This is the most straightforward path since you already have the API and the Swagger doc. The declarative agent's instructions would tell Copilot to always use that search endpoint to answer questions, cite the book and chapter, and so on.</p>
<p><strong>Option 2: Add MCP support alongside your existing API.</strong> You'd expose a <code>search_AcmeKnowledgebase</code> tool via MCP that does the same thing — takes a query, hits Azure AI Search, returns results. The .NET MCP SDK (it's the <code>Microsoft.Extensions.AI</code> ecosystem, with the <code>ModelContextProtocol</code> package) does make this pretty easy to bolt onto an existing Web API project. The advantage here is broader compatibility — any MCP-capable client could use it, not just Copilot. The downside is that MCP support in Microsoft 365 Copilot declarative agents is still in preview, so the API plugin route is more mature and stable for the Teams use case specifically.</p>
<p><strong>What I'd actually do in your position:</strong> start with the API plugin approach since it's GA and you already have all the pieces. Add one <code>POST /api/AcmeKnowledgebase/search</code> endpoint that queries Azure AI Search, update your OpenAPI spec to include it with a good description, and wire up a declarative agent. You could have something working in Teams within a day or two. Then if you want broader MCP compatibility later, add that as a second interface to the same underlying search logic — it's the same code underneath, just a different protocol on top.</p>
<p>The key point is that since Azure AI Search is already handling the hard part — figuring out which content is relevant across all your books and chapters — Copilot only ever needs to call that one search endpoint. It never needs to browse your book and chapter endpoints trying to find relevant content. That was your core concern, and your existing search index already solves it.</p>
</div>
<div class="question">
<p>I was trying to wire Copilot to Azure Search, but I like your idea better of hitting the Search in the API. We already have an endpoint that the Angular front end calls where the user types some text in an input, and we pass that value to the API in a REST call (can't remember if it is a POST or a GET). The code already calls Azure Search and returns the first 50 records it finds.</p>
<p>Just pointing Copilot to what we already have seems like a great idea, and very basic. I am bummed that I didn't think of going that route earlier. But, that is why you are here, right? ;-)</p>
</div>
<div class="answer">
<p>Ha, exactly — sometimes it just takes talking it through with someone to see what's already right in front of you. And honestly, the fact that you built the API-first architecture and already have Azure AI Search wired up means you did all the hard work already. You're literally one declarative agent configuration away from having this working in Teams.</p>
<p>The only thing I'd flag is that your existing endpoint returning 50 results is probably more than you want to send to Copilot. The LLM's context window is finite and you want it focused on the most relevant content, not sifting through 50 records. You might want to either add a parameter to that endpoint to limit the result count (top 5–10 is usually the sweet spot for RAG), or just create a slim variant of the endpoint specifically for the Copilot use case. That way your Angular front end keeps working exactly as it does now with its 50 results for the user to browse, and Copilot gets a tighter set of highly relevant chunks to reason over.</p>
<p>Other than that, you've got everything you need. Go wire it up — this should be a pretty satisfying win given how little work is left.</p>
</div>
<div class="question">
<p>Dang, it is Sunday, so I should be off work, but this sounds so fun, I almost want to go do it right now. But, I will resist and wait until tomorrow. Thanks for your help.</p>
</div>
<div class="answer">
<p>Ha, I respect the discipline! Enjoy your Sunday — and tomorrow you get to walk into work with a plan that basically amounts to &quot;we just need to point Copilot at what we already built.&quot; That's a pretty great Monday morning.</p>
<p>Good luck with it, Scott. Have a good rest of your weekend.</p>
</div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Connecting M365 Copilot to an On-Premises Knowledge Base Through Azure API Management</title>
      <link href="https://scottbrooks.info/posts/connecting-m365-copilot-to-an-on-premises-knowledge-base-through-azure-api-management/"/>
      <updated>2026-02-17T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/connecting-m365-copilot-to-an-on-premises-knowledge-base-through-azure-api-management/</id>
      <content type="html">
        <![CDATA[
      <div class="question">
<p>I would like anybody in our organization to be able to use M365 Copilot to ask questions from an internal knowledge store we have. We can access the KB multiple ways, but I am hoping one way is a REST API endpoint, which the KB already has. Can you provide me the steps required to create an Agent in M365 that can reach an OnPrem API that is not open to the internet?</p>
</div>
<div class="answer">
<h2>The Architecture at a Glance</h2>
<pre><code>M365 Copilot → Copilot Studio Agent → Custom Connector → Hybrid Bridge → Your On-Prem REST API
</code></pre>
<p>The main challenge is that last hop — getting from Microsoft's cloud into your network securely. There are a few options for that bridge, and the most common one is outlined below.</p>
<h2>Step-by-Step</h2>
<h3>1. Expose Your On-Prem API Securely (Without Opening It to the Internet)</h3>
<p>You have two primary options here:</p>
<p><strong>Option A — Azure Relay / Hybrid Connection (Recommended for simplicity)</strong></p>
<ul>
<li>Set up an <strong>Azure Relay</strong> resource in your Azure subscription.</li>
<li>Install the <strong>Hybrid Connection Manager</strong> on a server inside your network that can reach the KB API.</li>
<li>This creates an outbound-only connection from your network to Azure — no inbound firewall rules needed.</li>
<li>Your on-prem API becomes reachable from Azure services through the relay.</li>
</ul>
<p><strong>Option B — On-Premises Data Gateway (Power Platform native)</strong></p>
<ul>
<li>Install the <strong>Microsoft On-Premises Data Gateway</strong> on a machine inside your network.</li>
<li>This is the same gateway used by Power BI, Power Automate, etc.</li>
<li>It creates an outbound encrypted channel to the Power Platform.</li>
<li>This is natively supported by Power Platform custom connectors.</li>
</ul>
<p>Option B is often the path of least resistance since it's built into the Power Platform ecosystem that Copilot Studio sits on.</p>
<h3>2. Create a Custom Connector in Power Platform</h3>
<ul>
<li>Go to <strong>make.powerapps.com</strong> → <strong>Custom Connectors</strong> (under Data or Dataverse).</li>
<li>Create a new connector and point it at your API. You can import a Swagger/OpenAPI spec if your KB API has one, which makes this much easier.</li>
<li>Define the actions (e.g., <code>SearchKnowledgeBase</code>, <code>GetArticleById</code>) with the request/response schemas.</li>
<li>Under the <strong>General</strong> tab, set the host to your on-prem API endpoint. If using the on-prem data gateway, check the <strong>&quot;Connect via on-premises data gateway&quot;</strong> option.</li>
<li>Configure authentication to match whatever your KB API requires (API key, OAuth, basic auth, etc.).</li>
</ul>
<h3>3. Create a Power Automate Flow as Middleware (Optional but Recommended)</h3>
<p>This step gives you more control over how data flows:</p>
<ul>
<li>Create a <strong>Power Automate flow</strong> with an HTTP trigger (so Copilot Studio can call it).</li>
<li>In the flow, call your custom connector, pass the user's query to your KB API, and format the response.</li>
<li>This lets you add logic like filtering results, formatting citations, handling errors gracefully, etc.</li>
</ul>
<h3>4. Build the Copilot Studio Agent</h3>
<ul>
<li>Go to <strong>copilotstudio.microsoft.com</strong>.</li>
<li>Create a new agent (you can publish it to M365 Copilot later).</li>
<li>Define <strong>Topics</strong> or use the <strong>generative answers</strong> capability:
<ul>
<li>For a structured approach: create topics that capture the user's question, call your Power Automate flow or custom connector as an action, and return the response.</li>
<li>For a more natural approach: use <strong>Generative AI orchestration</strong> and register your connector/flow as a &quot;plugin action&quot; — Copilot will decide when to call it based on the user's question.</li>
</ul>
</li>
<li>Under <strong>Actions</strong>, add your Power Automate flow or custom connector directly.</li>
</ul>
<h3>5. Publish to M365 Copilot</h3>
<ul>
<li>In Copilot Studio, go to <strong>Channels</strong> → <strong>Microsoft 365 Copilot</strong>.</li>
<li>Enable the channel and publish.</li>
<li>An admin then approves the agent in the <strong>Microsoft 365 Admin Center</strong> → <strong>Integrated Apps</strong> section.</li>
<li>Once approved, users across your org can invoke the agent from within M365 Copilot (in Teams, the web experience, etc.) by @mentioning it or through the Copilot interface.</li>
</ul>
<h2>Key Decisions You'll Need to Make</h2>
<ul>
<li><strong>Gateway choice</strong>: The On-Premises Data Gateway is simpler for Power Platform. Azure Relay or Azure API Management gives you more flexibility if you want other services to use the same bridge.</li>
<li><strong>Authentication</strong>: How does your KB API authenticate requests? You'll need to replicate that in the custom connector.</li>
<li><strong>Response formatting</strong>: Copilot works best when it gets clean, well-structured text back. If your API returns complex JSON, the Power Automate flow layer is really helpful for shaping that into something the AI can reason over.</li>
</ul>
<h3>Licensing Note</h3>
<p>Copilot Studio agents that are published to M365 Copilot require either <strong>M365 Copilot licenses</strong> for the users accessing them, or <strong>Copilot Studio capacity</strong> depending on how you deploy. Worth checking with your M365 admin on what licenses you already have in play.</p>
</div>
<hr>
<div class="question">
<p>I am wondering if we might use Azure API Management (APIM) to reach the On-Prem API. How much would that change the steps you outlined?</p>
</div>
<div class="answer">
<p>APIM is a really strong choice, especially in an enterprise setting. It actually simplifies some steps while giving you a lot more control. The overall flow changes surprisingly little; it mainly replaces and consolidates Steps 1 and 2.</p>
<h2>Updated Architecture</h2>
<pre><code>M365 Copilot → Copilot Studio Agent → Custom Connector → Azure APIM → Self-Hosted Gateway → Your On-Prem REST API
</code></pre>
<p>The key piece is the <strong>APIM Self-Hosted Gateway</strong>, which is a containerized version of the Azure API Management gateway that you run inside your network.</p>
<h2>What Changes</h2>
<h3>Step 1 — Replace the Hybrid Bridge with APIM + Self-Hosted Gateway</h3>
<ul>
<li>Create an <strong>Azure API Management</strong> instance in your Azure subscription. The <strong>Developer</strong> or <strong>Standard v2</strong> tier works for this; Premium has built-in VNet integration but is expensive.</li>
<li>Deploy a <strong>Self-Hosted Gateway</strong> inside your on-prem network (runs as a Docker container or on Kubernetes). This gateway maintains an outbound connection to your APIM instance in Azure — again, no inbound firewall rules needed.</li>
<li>In APIM, import your KB's REST API definition (Swagger/OpenAPI spec, or define it manually).</li>
<li>Assign that API to the <strong>self-hosted gateway</strong> so requests route through your on-prem gateway rather than the Azure-managed one.</li>
</ul>
<p>This single step replaces both the hybrid bridge <em>and</em> a lot of what the custom connector definition was doing.</p>
<h3>Step 2 — The Custom Connector Gets Simpler</h3>
<ul>
<li>Your custom connector in Power Platform now just points to your <strong>APIM endpoint</strong> (e.g., <code>https://your-apim.azure-api.net/kb/search</code>), which is a public, managed, secured URL.</li>
<li>No need to check &quot;connect via on-premises data gateway&quot; — APIM handles the on-prem routing transparently.</li>
<li>Authentication becomes cleaner: you can use APIM's built-in <strong>subscription keys</strong>, <strong>OAuth validation</strong>, or <strong>managed identity</strong> on the front door, regardless of what your backend KB API uses. APIM handles the credential translation.</li>
</ul>
<h3>Steps 3–5 — Essentially Unchanged</h3>
<p>The Power Automate flow (if you use one), Copilot Studio agent setup, and M365 publishing all stay the same. They just talk to APIM instead of going through the on-prem data gateway.</p>
<h2>Why APIM Is Worth the Extra Setup</h2>
<ul>
<li><strong>Single front door</strong> — Other tools, apps, or agents in your org can also hit the same APIM endpoint. You're not locked into Power Platform.</li>
<li><strong>Policies</strong> — Rate limiting, caching, request/response transformation, all configurable without touching your backend API. The caching alone could meaningfully reduce load on your KB.</li>
<li><strong>Monitoring</strong> — Built-in Application Insights integration gives you full visibility into who's calling what, latency, error rates, etc.</li>
<li><strong>Security layering</strong> — You can enforce OAuth, IP filtering, JWT validation, or subscription keys at the APIM level, adding a security layer in front of your KB API without modifying it.</li>
<li><strong>Response shaping</strong> — APIM policies can transform your KB API's response format before it even reaches Copilot Studio, potentially eliminating the need for that Power Automate middleware layer entirely.</li>
</ul>
<h2>Cost Consideration</h2>
<p>The self-hosted gateway is included with most APIM tiers, but the APIM instance itself has a monthly cost. The <strong>Developer</strong> tier is around $50/month and fine for dev/test. <strong>Standard v2</strong> starts around $170/month and is better for production. Compare that to the on-prem data gateway, which is free but gives you far fewer capabilities.</p>
</div>
<hr>
<div class="question">
<p>We like the idea of the self-hosted APIM as an option in the future, but for this work, we would want to use the APIM in Azure.</p>
</div>
<div class="answer">
<p>That's a perfectly valid approach — and very common. But it does raise the key question: if APIM is running in Azure (not on-prem) and your KB API isn't open to the internet, <strong>how does APIM reach your backend?</strong></p>
<p>The self-hosted gateway was solving that problem. Without it, you need a network path from Azure to your on-prem network. Here are the options:</p>
<h2>Option A — Site-to-Site VPN (Most Common)</h2>
<ul>
<li>Your org sets up a <strong>Site-to-Site VPN</strong> between your on-prem network and an Azure Virtual Network (VNet).</li>
<li>APIM is deployed with <strong>VNet integration</strong> so it can route traffic through that VPN tunnel.</li>
<li>APIM calls your KB API's internal IP/hostname, and the traffic flows through the VPN — never touching the public internet.</li>
</ul>
<p><strong>Requirements:</strong></p>
<ul>
<li>A VPN gateway appliance on-prem (many enterprise firewalls already support this — Cisco, Palo Alto, Fortinet, etc.)</li>
<li>An Azure VPN Gateway resource</li>
<li>APIM on a tier that supports VNet integration (<strong>Developer</strong> or <strong>Premium</strong> for full VNet injection; <strong>Standard v2</strong> supports VNet integration in a lighter model)</li>
</ul>
<p><strong>Worth asking your network team:</strong> You may already have a Site-to-Site VPN or ExpressRoute connection to Azure. A lot of orgs with M365 Copilot and Azure AD already have this in place.</p>
<h2>Option B — Azure ExpressRoute</h2>
<ul>
<li>Same concept as the VPN, but uses a <strong>private dedicated circuit</strong> instead of an encrypted tunnel over the internet.</li>
<li>Higher bandwidth, lower latency, more predictable — but significantly more expensive and complex to set up.</li>
<li>If your org already has ExpressRoute for other Azure workloads, APIM can ride on it.</li>
</ul>
<h2>Option C — Azure Relay as the Bridge Behind APIM</h2>
<ul>
<li>If a VPN isn't in place and standing one up is a heavier lift than you want right now, you can use <strong>Azure Relay</strong> as the tunnel.</li>
<li>APIM's backend would point to the Azure Relay endpoint, and the Hybrid Connection Manager on-prem forwards requests to your KB API.</li>
<li>This keeps the &quot;no inbound firewall changes&quot; benefit and doesn't require VPN infrastructure.</li>
<li>The trade-off is a small amount of added latency and a dependency on the relay service.</li>
</ul>
<h2>How This Affects the Steps</h2>
<p>The Copilot Studio agent, custom connector, and publishing steps are all <strong>completely unchanged</strong> regardless of which option you pick. The only thing that changes is how APIM's backend is configured:</p>
<table>
<thead>
<tr>
<th>Approach</th>
<th>APIM Backend Setting</th>
<th>On-Prem Requirement</th>
</tr>
</thead>
<tbody>
<tr>
<td>Self-hosted gateway (future)</td>
<td>Routes through on-prem container</td>
<td>Docker/K8s host</td>
</tr>
<tr>
<td>Site-to-Site VPN</td>
<td>Internal IP/hostname of KB API</td>
<td>VPN appliance + Azure VPN Gateway</td>
</tr>
<tr>
<td>ExpressRoute</td>
<td>Internal IP/hostname of KB API</td>
<td>ExpressRoute circuit</td>
</tr>
<tr>
<td>Azure Relay</td>
<td>Azure Relay endpoint URL</td>
<td>Hybrid Connection Manager installed on-prem</td>
</tr>
</tbody>
</table>
</div>
<hr>
<div class="question">
<p>Our APIM is deployed with VNet integration. We have APIs already in APIM that require a Microsoft token to authenticate, and then once we know the request is valid, we use APIM policy to reach OnPrem servers.</p>
</div>
<div class="answer">
<p>That's the ideal scenario — the hard part is already done. You essentially have a proven pattern in place, and this becomes a matter of replicating it for your KB API and wiring up the Copilot Studio agent.</p>
<h2>Your Simplified Steps</h2>
<h3>1. Add Your KB API to APIM</h3>
<p>Since you've done this before with other APIs, this is familiar territory:</p>
<ul>
<li>Import your KB API into APIM (OpenAPI spec, or define operations manually).</li>
<li>Configure the <strong>backend</strong> to point to your on-prem KB server's internal hostname/IP — same as your other on-prem APIs.</li>
<li>Apply your existing <strong>APIM policy pattern</strong> for the on-prem hop (you likely already have a reusable policy fragment for this).</li>
<li>Set up the <strong>inbound authentication policy</strong> to validate Microsoft Entra ID (Azure AD) tokens, just like your other APIs. <code>validate-jwt</code> or <code>validate-azure-ad-token</code> policy — whatever you're already using.</li>
</ul>
<p>At this point, your KB API is callable from Azure, secured by a Microsoft token, and routed to on-prem through your existing VNet. Nothing new here.</p>
<h3>2. Create a Custom Connector in Power Platform</h3>
<ul>
<li>Go to <strong>make.powerapps.com</strong> → <strong>Custom Connectors</strong>.</li>
<li>Point it at your APIM endpoint (e.g., <code>https://your-apim.azure-api.net/kb</code>).</li>
<li>For authentication, configure <strong>OAuth 2.0 with Azure AD</strong> — register an app in Entra ID (or reuse one) that has permission to call your APIM. The connector will acquire a token and pass it with each request, which APIM validates just like it does for your other APIs.</li>
<li>Define the actions that map to your KB API operations — something like <code>SearchArticles(query: string)</code> and maybe <code>GetArticle(id: string)</code>.</li>
</ul>
<h3>3. Build the Copilot Studio Agent</h3>
<ul>
<li>Go to <strong>copilotstudio.microsoft.com</strong> and create a new agent.</li>
<li>Under <strong>Actions</strong>, add your custom connector.</li>
<li>Configure how the agent uses it — either through explicit <strong>Topics</strong> (&quot;When the user asks a knowledge question, call SearchArticles&quot;) or through <strong>generative orchestration</strong> (let the AI decide when to call it based on the user's intent).</li>
<li>The key thing to get right: the <strong>system prompt / instructions</strong> for the agent. Tell it what the KB contains, when to search it, and how to present results. This is where the quality of the experience lives.</li>
</ul>
<h3>4. Publish to M365 Copilot</h3>
<ul>
<li>In Copilot Studio → <strong>Channels</strong> → enable <strong>Microsoft 365 Copilot</strong>.</li>
<li>Publish, then have your M365 admin approve it in the <strong>Admin Center → Integrated Apps</strong>.</li>
<li>Users can then invoke it from within Copilot.</li>
</ul>
<h2>Summary</h2>
<p>Because your APIM infrastructure, VNet connectivity, and authentication pattern are already established, you've eliminated all the hard infrastructure questions. The work is:</p>
<ol>
<li><strong>One more API in APIM</strong> (routine for your team)</li>
<li><strong>One custom connector</strong> (straightforward since APIM gives you a clean, well-documented endpoint)</li>
<li><strong>One Copilot Studio agent</strong> (the new piece, but not complex)</li>
<li><strong>Publish and approve</strong> (admin workflow)</li>
</ol>
<p>The thing worth spending the most time on is that <strong>agent system prompt</strong> in Step 3 — how you instruct the agent to interpret user questions, when to call the KB vs. answer from general knowledge, and how to format/cite the KB responses. That's what makes the difference between a useful agent and a frustrating one.</p>
</div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Catio - Thinking in Components</title>
      <link href="https://scottbrooks.info/posts/catio-thinking-in-components/"/>
      <updated>2026-02-22T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/catio-thinking-in-components/</id>
      <content type="html">
        <![CDATA[
      <p>In October 2023, I built a catio (cat patio) to give our indoor cats a safe way to enjoy the outdoors. It was a fun project that took a few weeks to complete, and the cats absolutely love it!</p>
<p>Originally, none of the pictures had captions.  That seemed like a good thing to throw at Claude.  It took a couple attempts, but now we have captions.   However, many of the captions are pretty much the same.<br></p>
<p>Each photo has a longer description in its tooltip when you hover over it. Some of them turned out pretty funny!<br>
<img src="/images/catio/wheelchair.png"></p>
<p><a href="#prompts">Jump to Prompts →</a></p>
<h2>The Build</h2>
<p>Here are photos documenting the entire build process and the finished catio:</p>
<div class="photo-gallery">
<figure class="gallery-item">
  <a href="/images/catio/PXL_20230930_214457697.jpg" target="_blank" title="A person works on building a wooden frame for a DIY catio project, providing a safe outdoor space for indoor cats.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20230930_214457697-300w.webp 300w, /images/optimized/PXL_20230930_214457697-600w.webp 600w, /images/optimized/PXL_20230930_214457697-1200w.webp 1200w" sizes="300px"><img alt="Constructing a cozy outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20230930_214457697-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20230930_214457697-300w.jpeg 300w, /images/optimized/PXL_20230930_214457697-600w.jpeg 600w, /images/optimized/PXL_20230930_214457697-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing a cozy outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20230930_214502025.jpg" target="_blank" title="Modular wooden frame constructed for an enclosed outdoor cat patio, with space for enrichment features.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20230930_214502025-300w.webp 300w, /images/optimized/PXL_20230930_214502025-600w.webp 600w, /images/optimized/PXL_20230930_214502025-1200w.webp 1200w" sizes="300px"><img alt="Wooden frame for DIY cat patio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20230930_214502025-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20230930_214502025-300w.jpeg 300w, /images/optimized/PXL_20230930_214502025-600w.jpeg 600w, /images/optimized/PXL_20230930_214502025-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Wooden frame for DIY cat patio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20230930_214510493.jpg" target="_blank" title="Outdoor cat enclosure under construction, with wooden frame, wire mesh, and accessories for the feline residents.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20230930_214510493-300w.webp 300w, /images/optimized/PXL_20230930_214510493-600w.webp 600w, /images/optimized/PXL_20230930_214510493-1200w.webp 1200w" sizes="300px"><img alt="Constructing a DIY catio for cats" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20230930_214510493-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20230930_214510493-300w.jpeg 300w, /images/optimized/PXL_20230930_214510493-600w.jpeg 600w, /images/optimized/PXL_20230930_214510493-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing a DIY catio for cats</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20230930_214747658.jpg" target="_blank" title="A partially completed catio with a brick exterior wall and window, ready for additional features to be added.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20230930_214747658-300w.webp 300w, /images/optimized/PXL_20230930_214747658-600w.webp 600w, /images/optimized/PXL_20230930_214747658-1200w.webp 1200w" sizes="300px"><img alt="Brick-walled catio under construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20230930_214747658-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20230930_214747658-300w.jpeg 300w, /images/optimized/PXL_20230930_214747658-600w.jpeg 600w, /images/optimized/PXL_20230930_214747658-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Brick-walled catio under construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20230930_214804939.jpg" target="_blank" title="A brick home with a newly constructed outdoor catio enclosure in the grassy yard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20230930_214804939-300w.webp 300w, /images/optimized/PXL_20230930_214804939-600w.webp 600w, /images/optimized/PXL_20230930_214804939-1200w.webp 1200w" sizes="300px"><img alt="Brick home with grassy catio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20230930_214804939-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20230930_214804939-300w.jpeg 300w, /images/optimized/PXL_20230930_214804939-600w.jpeg 600w, /images/optimized/PXL_20230930_214804939-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Brick home with grassy catio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_174935912.jpg" target="_blank" title="Man building a wooden frame for an outdoor cat enclosure, or "catio", to allow indoor cats to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_174935912-300w.webp 300w, /images/optimized/PXL_20231008_174935912-600w.webp 600w, /images/optimized/PXL_20231008_174935912-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_174935912-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_174935912-300w.jpeg 300w, /images/optimized/PXL_20231008_174935912-600w.jpeg 600w, /images/optimized/PXL_20231008_174935912-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_174940584.jpg" target="_blank" title="A person is shown working on the construction of an enclosed outdoor area, or "catio", for indoor cats to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_174940584-300w.webp 300w, /images/optimized/PXL_20231008_174940584-600w.webp 600w, /images/optimized/PXL_20231008_174940584-1200w.webp 1200w" sizes="300px"><img alt="Constructing an enclosed outdoor cat space" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_174940584-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_174940584-300w.jpeg 300w, /images/optimized/PXL_20231008_174940584-600w.jpeg 600w, /images/optimized/PXL_20231008_174940584-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing an enclosed outdoor cat space</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_174949056.jpg" target="_blank" title="A person works on the wooden frame of a catio, an outdoor space that allows indoor cats to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_174949056-300w.webp 300w, /images/optimized/PXL_20231008_174949056-600w.webp 600w, /images/optimized/PXL_20231008_174949056-1200w.webp 1200w" sizes="300px"><img alt="Constructing an outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_174949056-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_174949056-300w.jpeg 300w, /images/optimized/PXL_20231008_174949056-600w.jpeg 600w, /images/optimized/PXL_20231008_174949056-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing an outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_174955040.jpg" target="_blank" title="A partially assembled wooden frame for a modular cat patio stands in a grassy backyard surrounded by trees.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_174955040-300w.webp 300w, /images/optimized/PXL_20231008_174955040-600w.webp 600w, /images/optimized/PXL_20231008_174955040-1200w.webp 1200w" sizes="300px"><img alt="Modular catio frame under construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_174955040-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_174955040-300w.jpeg 300w, /images/optimized/PXL_20231008_174955040-600w.jpeg 600w, /images/optimized/PXL_20231008_174955040-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Modular catio frame under construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_174957654.jpg" target="_blank" title="Wooden frame and power tools for constructing a cat patio in a backyard with trees and grass.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_174957654-300w.webp 300w, /images/optimized/PXL_20231008_174957654-600w.webp 600w, /images/optimized/PXL_20231008_174957654-1200w.webp 1200w" sizes="300px"><img alt="Building an outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_174957654-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_174957654-300w.jpeg 300w, /images/optimized/PXL_20231008_174957654-600w.jpeg 600w, /images/optimized/PXL_20231008_174957654-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Building an outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_204650143.jpg" target="_blank" title="A brick-faced home undergoing a catio construction project, with ladders, lumber, and other building supplies visible on the lawn.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_204650143-300w.webp 300w, /images/optimized/PXL_20231008_204650143-600w.webp 600w, /images/optimized/PXL_20231008_204650143-1200w.webp 1200w" sizes="300px"><img alt="Catio construction with tools and materials" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_204650143-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_204650143-300w.jpeg 300w, /images/optimized/PXL_20231008_204650143-600w.jpeg 600w, /images/optimized/PXL_20231008_204650143-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction with tools and materials</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_204703886.jpg" target="_blank" title="A brick-walled home with a partially built wooden frame structure for an outdoor cat enclosure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_204703886-300w.webp 300w, /images/optimized/PXL_20231008_204703886-600w.webp 600w, /images/optimized/PXL_20231008_204703886-1200w.webp 1200w" sizes="300px"><img alt="DIY cat patio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_204703886-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_204703886-300w.jpeg 300w, /images/optimized/PXL_20231008_204703886-600w.jpeg 600w, /images/optimized/PXL_20231008_204703886-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY cat patio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231008_204742067.jpg" target="_blank" title="An outdoor enclosure takes shape as part of a DIY catio project to give indoor cats safe outdoor access.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231008_204742067-300w.webp 300w, /images/optimized/PXL_20231008_204742067-600w.webp 600w, /images/optimized/PXL_20231008_204742067-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction underway" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231008_204742067-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231008_204742067-300w.jpeg 300w, /images/optimized/PXL_20231008_204742067-600w.jpeg 600w, /images/optimized/PXL_20231008_204742067-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction underway</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231009_155758399.jpg" target="_blank" title="Wooden frame and mesh walls for a cat patio project, with ladders and construction materials visible in the yard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231009_155758399-300w.webp 300w, /images/optimized/PXL_20231009_155758399-600w.webp 600w, /images/optimized/PXL_20231009_155758399-1200w.webp 1200w" sizes="300px"><img alt="Catio construction with ladders and materials" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231009_155758399-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231009_155758399-300w.jpeg 300w, /images/optimized/PXL_20231009_155758399-600w.jpeg 600w, /images/optimized/PXL_20231009_155758399-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction with ladders and materials</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231009_190740266.jpg" target="_blank" title="Outdoor catio space under construction, with wooden frame, ladders, and various DIY tools visible.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231009_190740266-300w.webp 300w, /images/optimized/PXL_20231009_190740266-600w.webp 600w, /images/optimized/PXL_20231009_190740266-1200w.webp 1200w" sizes="300px"><img alt="Catio construction with ladders and tools" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231009_190740266-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231009_190740266-300w.jpeg 300w, /images/optimized/PXL_20231009_190740266-600w.jpeg 600w, /images/optimized/PXL_20231009_190740266-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction with ladders and tools</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231009_194252055.jpg" target="_blank" title="A homeowner builds a wooden frame and mesh walls for a backyard catio to allow indoor cats to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231009_194252055-300w.webp 300w, /images/optimized/PXL_20231009_194252055-600w.webp 600w, /images/optimized/PXL_20231009_194252055-1200w.webp 1200w" sizes="300px"><img alt="Constructing a custom outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231009_194252055-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231009_194252055-300w.jpeg 300w, /images/optimized/PXL_20231009_194252055-600w.jpeg 600w, /images/optimized/PXL_20231009_194252055-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing a custom outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_114415077.jpg" target="_blank" title="Partially completed catio frame with wooden posts and beams installed along a brick building exterior.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_114415077-300w.webp 300w, /images/optimized/PXL_20231010_114415077-600w.webp 600w, /images/optimized/PXL_20231010_114415077-1200w.webp 1200w" sizes="300px"><img alt="Outdoor cat enclosure under construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_114415077-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_114415077-300w.jpeg 300w, /images/optimized/PXL_20231010_114415077-600w.jpeg 600w, /images/optimized/PXL_20231010_114415077-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Outdoor cat enclosure under construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_230050678.jpg" target="_blank" title="Outdoor workspace with tools, ladder, and materials for building an enclosed cat patio in the backyard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_230050678-300w.webp 300w, /images/optimized/PXL_20231010_230050678-600w.webp 600w, /images/optimized/PXL_20231010_230050678-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in backyard" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_230050678-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_230050678-300w.jpeg 300w, /images/optimized/PXL_20231010_230050678-600w.jpeg 600w, /images/optimized/PXL_20231010_230050678-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in backyard</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_230054523.jpg" target="_blank" title="Wooden frame and ladders used to build a secure outdoor cat patio in the backyard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_230054523-300w.webp 300w, /images/optimized/PXL_20231010_230054523-600w.webp 600w, /images/optimized/PXL_20231010_230054523-1200w.webp 1200w" sizes="300px"><img alt="Constructing an outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_230054523-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_230054523-300w.jpeg 300w, /images/optimized/PXL_20231010_230054523-600w.jpeg 600w, /images/optimized/PXL_20231010_230054523-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing an outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_230102814.jpg" target="_blank" title="A bearded man works on building a wooden frame for an outdoor cat enclosure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_230102814-300w.webp 300w, /images/optimized/PXL_20231010_230102814-600w.webp 600w, /images/optimized/PXL_20231010_230102814-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_230102814-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_230102814-300w.jpeg 300w, /images/optimized/PXL_20231010_230102814-600w.jpeg 600w, /images/optimized/PXL_20231010_230102814-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_230145004.jpg" target="_blank" title="A handyman builds a sturdy wooden frame for an outdoor cat patio using power tools.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_230145004-300w.webp 300w, /images/optimized/PXL_20231010_230145004-600w.webp 600w, /images/optimized/PXL_20231010_230145004-1200w.webp 1200w" sizes="300px"><img alt="Constructing a cozy outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_230145004-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_230145004-300w.jpeg 300w, /images/optimized/PXL_20231010_230145004-600w.jpeg 600w, /images/optimized/PXL_20231010_230145004-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing a cozy outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_230205442.jpg" target="_blank" title="Outdoor enclosure being built for cats to safely enjoy the outdoors, with a wooden frame, wire mesh, and various construction materials visible.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_230205442-300w.webp 300w, /images/optimized/PXL_20231010_230205442-600w.webp 600w, /images/optimized/PXL_20231010_230205442-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_230205442-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_230205442-300w.jpeg 300w, /images/optimized/PXL_20231010_230205442-600w.jpeg 600w, /images/optimized/PXL_20231010_230205442-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231010_230214994.jpg" target="_blank" title="A homeowner constructs an enclosed outdoor space for indoor cats to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231010_230214994-300w.webp 300w, /images/optimized/PXL_20231010_230214994-600w.webp 600w, /images/optimized/PXL_20231010_230214994-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231010_230214994-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231010_230214994-300w.jpeg 300w, /images/optimized/PXL_20231010_230214994-600w.jpeg 600w, /images/optimized/PXL_20231010_230214994-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232746011.jpg" target="_blank" title="Construction of an outdoor enclosure to allow indoor cats to safely enjoy the outdoors, with workers on ladders and materials visible.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232746011-300w.webp 300w, /images/optimized/PXL_20231012_232746011-600w.webp 600w, /images/optimized/PXL_20231012_232746011-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232746011-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232746011-300w.jpeg 300w, /images/optimized/PXL_20231012_232746011-600w.jpeg 600w, /images/optimized/PXL_20231012_232746011-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232759289.jpg" target="_blank" title="A wired wall-mounted electrical box provides power and controls for the catio lighting and heating features.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232759289-300w.webp 300w, /images/optimized/PXL_20231012_232759289-600w.webp 600w, /images/optimized/PXL_20231012_232759289-1200w.webp 1200w" sizes="300px"><img alt="Outdoor enclosure for feline safety" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232759289-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232759289-300w.jpeg 300w, /images/optimized/PXL_20231012_232759289-600w.jpeg 600w, /images/optimized/PXL_20231012_232759289-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Outdoor enclosure for feline safety</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232810964.jpg" target="_blank" title="A person builds a wooden frame for a catio, an enclosed outdoor space for indoor cats, under the night sky.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232810964-300w.webp 300w, /images/optimized/PXL_20231012_232810964-600w.webp 600w, /images/optimized/PXL_20231012_232810964-1200w.webp 1200w" sizes="300px"><img alt="Constructing outdoor cat enclosure at night" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232810964-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232810964-300w.jpeg 300w, /images/optimized/PXL_20231012_232810964-600w.jpeg 600w, /images/optimized/PXL_20231012_232810964-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing outdoor cat enclosure at night</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232828783.jpg" target="_blank" title="Workers building an outdoor enclosure for pet cats to safely enjoy the outdoors at night.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232828783-300w.webp 300w, /images/optimized/PXL_20231012_232828783-600w.webp 600w, /images/optimized/PXL_20231012_232828783-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction underway at night" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232828783-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232828783-300w.jpeg 300w, /images/optimized/PXL_20231012_232828783-600w.jpeg 600w, /images/optimized/PXL_20231012_232828783-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction underway at night</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232832955.jpg" target="_blank" title="Homeowner builds an enclosed outdoor cat patio using ladders, tools, and construction materials on the exterior of the house.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232832955-300w.webp 300w, /images/optimized/PXL_20231012_232832955-600w.webp 600w, /images/optimized/PXL_20231012_232832955-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232832955-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232832955-300w.jpeg 300w, /images/optimized/PXL_20231012_232832955-600w.jpeg 600w, /images/optimized/PXL_20231012_232832955-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232839072.jpg" target="_blank" title="A partially completed catio with brick walls and exterior lighting, an ongoing project to give indoor cats safe outdoor access.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232839072-300w.webp 300w, /images/optimized/PXL_20231012_232839072-600w.webp 600w, /images/optimized/PXL_20231012_232839072-1200w.webp 1200w" sizes="300px"><img alt="Nighttime catio construction with brick walls" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232839072-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232839072-300w.jpeg 300w, /images/optimized/PXL_20231012_232839072-600w.jpeg 600w, /images/optimized/PXL_20231012_232839072-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Nighttime catio construction with brick walls</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232844647.jpg" target="_blank" title="Partially built brick and wood catio structure with lights, ladders, and open window for indoor cats to enjoy.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232844647-300w.webp 300w, /images/optimized/PXL_20231012_232844647-600w.webp 600w, /images/optimized/PXL_20231012_232844647-1200w.webp 1200w" sizes="300px"><img alt="Cozy DIY catio under construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232844647-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232844647-300w.jpeg 300w, /images/optimized/PXL_20231012_232844647-600w.jpeg 600w, /images/optimized/PXL_20231012_232844647-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy DIY catio under construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232903854.jpg" target="_blank" title="A worker attaches wire mesh to the wooden frame of the newly built catio structure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232903854-300w.webp 300w, /images/optimized/PXL_20231012_232903854-600w.webp 600w, /images/optimized/PXL_20231012_232903854-1200w.webp 1200w" sizes="300px"><img alt="Constructing the catio wooden frame" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232903854-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232903854-300w.jpeg 300w, /images/optimized/PXL_20231012_232903854-600w.jpeg 600w, /images/optimized/PXL_20231012_232903854-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing the catio wooden frame</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231012_232929863.jpg" target="_blank" title="Constructing a modular wooden frame and wire mesh catio to provide an enriching outdoor space for indoor cats.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231012_232929863-300w.webp 300w, /images/optimized/PXL_20231012_232929863-600w.webp 600w, /images/optimized/PXL_20231012_232929863-1200w.webp 1200w" sizes="300px"><img alt="Building a cozy outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231012_232929863-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231012_232929863-300w.jpeg 300w, /images/optimized/PXL_20231012_232929863-600w.jpeg 600w, /images/optimized/PXL_20231012_232929863-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Building a cozy outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_161730559.jpg" target="_blank" title="Outdoor enclosure under construction to provide safe outdoor access for indoor cats.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_161730559-300w.webp 300w, /images/optimized/PXL_20231013_161730559-600w.webp 600w, /images/optimized/PXL_20231013_161730559-1200w.webp 1200w" sizes="300px"><img alt="Catio construction in progress, October 2023" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_161730559-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_161730559-300w.jpeg 300w, /images/optimized/PXL_20231013_161730559-600w.jpeg 600w, /images/optimized/PXL_20231013_161730559-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction in progress, October 2023</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_161750312.jpg" target="_blank" title="Brick home with partially constructed catio structure, ladders, and various tools and materials in the yard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_161750312-300w.webp 300w, /images/optimized/PXL_20231013_161750312-600w.webp 600w, /images/optimized/PXL_20231013_161750312-1200w.webp 1200w" sizes="300px"><img alt="DIY catio project nearing completion" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_161750312-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_161750312-300w.jpeg 300w, /images/optimized/PXL_20231013_161750312-600w.jpeg 600w, /images/optimized/PXL_20231013_161750312-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio project nearing completion</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_181819544.jpg" target="_blank" title="A man works on building a wooden frame for an enclosed outdoor cat space, or "catio", to allow indoor cats to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_181819544-300w.webp 300w, /images/optimized/PXL_20231013_181819544-600w.webp 600w, /images/optimized/PXL_20231013_181819544-1200w.webp 1200w" sizes="300px"><img alt="Constructing DIY catio for outdoor access" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_181819544-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_181819544-300w.jpeg 300w, /images/optimized/PXL_20231013_181819544-600w.jpeg 600w, /images/optimized/PXL_20231013_181819544-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing DIY catio for outdoor access</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_181822337.jpg" target="_blank" title="A partially completed wooden catio frame with stone pavers and chairs in the grassy outdoor space.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_181822337-300w.webp 300w, /images/optimized/PXL_20231013_181822337-600w.webp 600w, /images/optimized/PXL_20231013_181822337-1200w.webp 1200w" sizes="300px"><img alt="Modular catio frame with stone accents" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_181822337-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_181822337-300w.jpeg 300w, /images/optimized/PXL_20231013_181822337-600w.jpeg 600w, /images/optimized/PXL_20231013_181822337-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Modular catio frame with stone accents</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_182025554.jpg" target="_blank" title="A veteran constructing a brick-walled catio with various cat-friendly features for their indoor pets to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_182025554-300w.webp 300w, /images/optimized/PXL_20231013_182025554-600w.webp 600w, /images/optimized/PXL_20231013_182025554-1200w.webp 1200w" sizes="300px"><img alt="Veteran building outdoor cat enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_182025554-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_182025554-300w.jpeg 300w, /images/optimized/PXL_20231013_182025554-600w.jpeg 600w, /images/optimized/PXL_20231013_182025554-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Veteran building outdoor cat enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_182035642.jpg" target="_blank" title="A person digging soil to prepare for a catio, a wooden enclosure for indoor cats to safely enjoy the outdoors, as their dog watches nearby.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_182035642-300w.webp 300w, /images/optimized/PXL_20231013_182035642-600w.webp 600w, /images/optimized/PXL_20231013_182035642-1200w.webp 1200w" sizes="300px"><img alt="Constructing a catio for indoor cats" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_182035642-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_182035642-300w.jpeg 300w, /images/optimized/PXL_20231013_182035642-600w.jpeg 600w, /images/optimized/PXL_20231013_182035642-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Constructing a catio for indoor cats</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231013_182216028.jpg" target="_blank" title="A dog relaxes in the grass as a person works on building a catio enclosure for cats nearby.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231013_182216028-300w.webp 300w, /images/optimized/PXL_20231013_182216028-600w.webp 600w, /images/optimized/PXL_20231013_182216028-1200w.webp 1200w" sizes="300px"><img alt="Catio construction project with canine helper" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231013_182216028-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231013_182216028-300w.jpeg 300w, /images/optimized/PXL_20231013_182216028-600w.jpeg 600w, /images/optimized/PXL_20231013_182216028-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction project with canine helper</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231017_144257948.jpg" target="_blank" title="Outdoor cat enclosure with wooden frame and wire mesh panels, under construction in a grassy backyard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231017_144257948-300w.webp 300w, /images/optimized/PXL_20231017_144257948-600w.webp 600w, /images/optimized/PXL_20231017_144257948-1200w.webp 1200w" sizes="300px"><img alt="Catio construction with wooden frame and mesh" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231017_144257948-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231017_144257948-300w.jpeg 300w, /images/optimized/PXL_20231017_144257948-600w.jpeg 600w, /images/optimized/PXL_20231017_144257948-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction with wooden frame and mesh</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231017_144315611.jpg" target="_blank" title="Catio under construction, with a wooden frame, wire mesh walls, and a ladder for access.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231017_144315611-300w.webp 300w, /images/optimized/PXL_20231017_144315611-600w.webp 600w, /images/optimized/PXL_20231017_144315611-1200w.webp 1200w" sizes="300px"><img alt="Catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231017_144315611-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231017_144315611-300w.jpeg 300w, /images/optimized/PXL_20231017_144315611-600w.jpeg 600w, /images/optimized/PXL_20231017_144315611-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231017_144325521.jpg" target="_blank" title="Brick house with an in-progress catio project, featuring a wooden frame, wire mesh walls, and various construction supplies.">
      <picture><source type="image/webp" srcset="/images/optimized/PXL_20231017_144325521-300w.webp 300w, /images/optimized/PXL_20231017_144325521-600w.webp 600w, /images/optimized/PXL_20231017_144325521-1200w.webp 1200w" sizes="300px"><img alt="Brick home's outdoor catio construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231017_144325521-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231017_144325521-300w.jpeg 300w, /images/optimized/PXL_20231017_144325521-600w.jpeg 600w, /images/optimized/PXL_20231017_144325521-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Brick home's outdoor catio construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231017_144342392.jpg" target="_blank" title="Wooden frame and wire mesh enclosure in a grassy outdoor area, with tools and supplies nearby.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231017_144342392-300w.webp 300w, /images/optimized/PXL_20231017_144342392-600w.webp 600w, /images/optimized/PXL_20231017_144342392-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231017_144342392-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231017_144342392-300w.jpeg 300w, /images/optimized/PXL_20231017_144342392-600w.jpeg 600w, /images/optimized/PXL_20231017_144342392-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231017_144353146.jpg" target="_blank" title="An in-progress shot of a brick-walled catio structure with a ladder and window visible.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231017_144353146-300w.webp 300w, /images/optimized/PXL_20231017_144353146-600w.webp 600w, /images/optimized/PXL_20231017_144353146-1200w.webp 1200w" sizes="300px"><img alt="DIY catio construction in progress" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231017_144353146-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231017_144353146-300w.jpeg 300w, /images/optimized/PXL_20231017_144353146-600w.jpeg 600w, /images/optimized/PXL_20231017_144353146-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>DIY catio construction in progress</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231020_222903466.jpg" target="_blank" title="A cat lounges comfortably in a custom-built outdoor cat patio, with views of the yard through large windows.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231020_222903466-300w.webp 300w, /images/optimized/PXL_20231020_222903466-600w.webp 600w, /images/optimized/PXL_20231020_222903466-1200w.webp 1200w" sizes="300px"><img alt="Feline enjoys cozy outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231020_222903466-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231020_222903466-300w.jpeg 300w, /images/optimized/PXL_20231020_222903466-600w.jpeg 600w, /images/optimized/PXL_20231020_222903466-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Feline enjoys cozy outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231020_222908707.jpg" target="_blank" title="A cat relaxes on a wooden platform inside the newly constructed catio, with a view of the grassy backyard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231020_222908707-300w.webp 300w, /images/optimized/PXL_20231020_222908707-600w.webp 600w, /images/optimized/PXL_20231020_222908707-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy new enclosed outdoor space" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231020_222908707-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231020_222908707-300w.jpeg 300w, /images/optimized/PXL_20231020_222908707-600w.jpeg 600w, /images/optimized/PXL_20231020_222908707-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy new enclosed outdoor space</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231020_233417914.jpg" target="_blank" title="Fenced cat patio with wooden frames and wire mesh, featuring a grassy floor for the resident cats to explore.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231020_233417914-300w.webp 300w, /images/optimized/PXL_20231020_233417914-600w.webp 600w, /images/optimized/PXL_20231020_233417914-1200w.webp 1200w" sizes="300px"><img alt="Outdoor cat enclosure with artificial grass" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231020_233417914-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231020_233417914-300w.jpeg 300w, /images/optimized/PXL_20231020_233417914-600w.jpeg 600w, /images/optimized/PXL_20231020_233417914-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Outdoor cat enclosure with artificial grass</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231021_224253442.jpg" target="_blank" title="Brick-walled cat patio with wire mesh windows and shelves for feline residents to safely enjoy the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231021_224253442-300w.webp 300w, /images/optimized/PXL_20231021_224253442-600w.webp 600w, /images/optimized/PXL_20231021_224253442-1200w.webp 1200w" sizes="300px"><img alt="Cozy cat enclosure with platforms" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231021_224253442-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231021_224253442-300w.jpeg 300w, /images/optimized/PXL_20231021_224253442-600w.jpeg 600w, /images/optimized/PXL_20231021_224253442-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy cat enclosure with platforms</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231021_224302947.jpg" target="_blank" title="A custom-built catio attached to a brick house, providing a secure and enriched outdoor space for cats.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231021_224302947-300w.webp 300w, /images/optimized/PXL_20231021_224302947-600w.webp 600w, /images/optimized/PXL_20231021_224302947-1200w.webp 1200w" sizes="300px"><img alt="Cozy outdoor cat enclosure with platforms." loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231021_224302947-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231021_224302947-300w.jpeg 300w, /images/optimized/PXL_20231021_224302947-600w.jpeg 600w, /images/optimized/PXL_20231021_224302947-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy outdoor cat enclosure with platforms.</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231021_224325517.jpg" target="_blank" title="Custom-built catio with wire mesh walls and roof provides safe outdoor space for pet cats.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231021_224325517-300w.webp 300w, /images/optimized/PXL_20231021_224325517-600w.webp 600w, /images/optimized/PXL_20231021_224325517-1200w.webp 1200w" sizes="300px"><img alt="Modular cat enclosure for outdoor access" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231021_224325517-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231021_224325517-300w.jpeg 300w, /images/optimized/PXL_20231021_224325517-600w.jpeg 600w, /images/optimized/PXL_20231021_224325517-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Modular cat enclosure for outdoor access</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231021_224336395.jpg" target="_blank" title="Partially completed catio with wire mesh walls and roof, ladder, and assorted building materials on the grass.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231021_224336395-300w.webp 300w, /images/optimized/PXL_20231021_224336395-600w.webp 600w, /images/optimized/PXL_20231021_224336395-1200w.webp 1200w" sizes="300px"><img alt="Outdoor cat enclosure under construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231021_224336395-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231021_224336395-300w.jpeg 300w, /images/optimized/PXL_20231021_224336395-600w.jpeg 600w, /images/optimized/PXL_20231021_224336395-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Outdoor cat enclosure under construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231025_202530661.jpg" target="_blank" title="A DIY catio allows two cats to safely explore the outdoors while remaining enclosed in a secure mesh-walled structure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231025_202530661-300w.webp 300w, /images/optimized/PXL_20231025_202530661-600w.webp 600w, /images/optimized/PXL_20231025_202530661-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy their new outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231025_202530661-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231025_202530661-300w.jpeg 300w, /images/optimized/PXL_20231025_202530661-600w.jpeg 600w, /images/optimized/PXL_20231025_202530661-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy their new outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231025_202705167.jpg" target="_blank" title="Two cats resting in a custom-built outdoor catio attached to a home, providing them safe access to the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231025_202705167-300w.webp 300w, /images/optimized/PXL_20231025_202705167-600w.webp 600w, /images/optimized/PXL_20231025_202705167-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy outdoor catio enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231025_202705167-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231025_202705167-300w.jpeg 300w, /images/optimized/PXL_20231025_202705167-600w.jpeg 600w, /images/optimized/PXL_20231025_202705167-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy outdoor catio enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231025_202715178.jpg" target="_blank" title="A catio allows indoor cats to safely experience the outdoors while contained in a secure, enriched enclosure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231025_202715178-300w.webp 300w, /images/optimized/PXL_20231025_202715178-600w.webp 600w, /images/optimized/PXL_20231025_202715178-1200w.webp 1200w" sizes="300px"><img alt="Cats safely enjoy outdoor space" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231025_202715178-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231025_202715178-300w.jpeg 300w, /images/optimized/PXL_20231025_202715178-600w.jpeg 600w, /images/optimized/PXL_20231025_202715178-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats safely enjoy outdoor space</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231026_004814869.jpg" target="_blank" title="A backlit catio with warm lighting, festive string lights, and a glimpse of autumn foliage at night.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231026_004814869-300w.webp 300w, /images/optimized/PXL_20231026_004814869-600w.webp 600w, /images/optimized/PXL_20231026_004814869-1200w.webp 1200w" sizes="300px"><img alt="Cozy outdoor oasis for indoor cats" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231026_004814869-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231026_004814869-300w.jpeg 300w, /images/optimized/PXL_20231026_004814869-600w.jpeg 600w, /images/optimized/PXL_20231026_004814869-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy outdoor oasis for indoor cats</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231026_004818559.jpg" target="_blank" title="Partially enclosed outdoor area with wooden frame and wire mesh, outfitted with lighting to create a safe and comfortable space for cats.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231026_004818559-300w.webp 300w, /images/optimized/PXL_20231026_004818559-600w.webp 600w, /images/optimized/PXL_20231026_004818559-1200w.webp 1200w" sizes="300px"><img alt="Cozy cat catio with lighting" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231026_004818559-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231026_004818559-300w.jpeg 300w, /images/optimized/PXL_20231026_004818559-600w.jpeg 600w, /images/optimized/PXL_20231026_004818559-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy cat catio with lighting</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231026_004821557.jpg" target="_blank" title="Partially constructed catio frame with lighting and partially installed wire mesh walls.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231026_004821557-300w.webp 300w, /images/optimized/PXL_20231026_004821557-600w.webp 600w, /images/optimized/PXL_20231026_004821557-1200w.webp 1200w" sizes="300px"><img alt="Cozy outdoor cat enclosure takes shape" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231026_004821557-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231026_004821557-300w.jpeg 300w, /images/optimized/PXL_20231026_004821557-600w.jpeg 600w, /images/optimized/PXL_20231026_004821557-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy outdoor cat enclosure takes shape</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231026_004927469.jpg" target="_blank" title="Wooden framing for an outdoor cat enclosure, with bricks and wiring visible, during the building process.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231026_004927469-300w.webp 300w, /images/optimized/PXL_20231026_004927469-600w.webp 600w, /images/optimized/PXL_20231026_004927469-1200w.webp 1200w" sizes="300px"><img alt="Sturdy catio frame under construction" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231026_004927469-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231026_004927469-300w.jpeg 300w, /images/optimized/PXL_20231026_004927469-600w.jpeg 600w, /images/optimized/PXL_20231026_004927469-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Sturdy catio frame under construction</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231026_004935159.jpg" target="_blank" title="Red wooden frame and brick walls taking shape for an outdoor cat enclosure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231026_004935159-300w.webp 300w, /images/optimized/PXL_20231026_004935159-600w.webp 600w, /images/optimized/PXL_20231026_004935159-1200w.webp 1200w" sizes="300px"><img alt="Cozy catio construction underway" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231026_004935159-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231026_004935159-300w.jpeg 300w, /images/optimized/PXL_20231026_004935159-600w.jpeg 600w, /images/optimized/PXL_20231026_004935159-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy catio construction underway</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_001514600.jpg" target="_blank" title="A playful cat cautiously surveys its new outdoor catio, a safe and enriching space for indoor felines.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_001514600-300w.webp 300w, /images/optimized/PXL_20231028_001514600-600w.webp 600w, /images/optimized/PXL_20231028_001514600-1200w.webp 1200w" sizes="300px"><img alt="Curious cat explores new outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_001514600-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_001514600-300w.jpeg 300w, /images/optimized/PXL_20231028_001514600-600w.jpeg 600w, /images/optimized/PXL_20231028_001514600-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Curious cat explores new outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_170954110.jpg" target="_blank" title="A cat stands on a wooden platform inside a secure outdoor enclosure, gazing through the wire mesh at the autumn trees beyond.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_170954110-300w.webp 300w, /images/optimized/PXL_20231028_170954110-600w.webp 600w, /images/optimized/PXL_20231028_170954110-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy outdoor enclosure in fall" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_170954110-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_170954110-300w.jpeg 300w, /images/optimized/PXL_20231028_170954110-600w.jpeg 600w, /images/optimized/PXL_20231028_170954110-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy outdoor enclosure in fall</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_170955740.jpg" target="_blank" title="A cat stands atop the wooden structure of a newly constructed outdoor enclosure, surveying the colorful autumn scenery beyond the wire mesh.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_170955740-300w.webp 300w, /images/optimized/PXL_20231028_170955740-600w.webp 600w, /images/optimized/PXL_20231028_170955740-1200w.webp 1200w" sizes="300px"><img alt="Feline explorer enjoys DIY catio view" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_170955740-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_170955740-300w.jpeg 300w, /images/optimized/PXL_20231028_170955740-600w.jpeg 600w, /images/optimized/PXL_20231028_170955740-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Feline explorer enjoys DIY catio view</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_171213351.jpg" target="_blank" title="A cat peeks out from behind the wire mesh of a newly constructed outdoor enclosure, or "catio", designed to allow safe outdoor access.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_171213351-300w.webp 300w, /images/optimized/PXL_20231028_171213351-600w.webp 600w, /images/optimized/PXL_20231028_171213351-1200w.webp 1200w" sizes="300px"><img alt="Curious cat explores new outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_171213351-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_171213351-300w.jpeg 300w, /images/optimized/PXL_20231028_171213351-600w.jpeg 600w, /images/optimized/PXL_20231028_171213351-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Curious cat explores new outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_171522627.jpg" target="_blank" title="A cat perched on a raised platform inside a wooden-framed catio with mesh walls, allowing safe outdoor access.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_171522627-300w.webp 300w, /images/optimized/PXL_20231028_171522627-600w.webp 600w, /images/optimized/PXL_20231028_171522627-1200w.webp 1200w" sizes="300px"><img alt="Cats explore custom-built outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_171522627-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_171522627-300w.jpeg 300w, /images/optimized/PXL_20231028_171522627-600w.jpeg 600w, /images/optimized/PXL_20231028_171522627-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats explore custom-built outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_171531295.jpg" target="_blank" title="Two cats relaxing on a wooden platform inside a DIY cat patio with mesh walls and windows overlooking a grassy outdoor area.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_171531295-300w.webp 300w, /images/optimized/PXL_20231028_171531295-600w.webp 600w, /images/optimized/PXL_20231028_171531295-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy outdoor enclosure's cozy perch" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_171531295-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_171531295-300w.jpeg 300w, /images/optimized/PXL_20231028_171531295-600w.jpeg 600w, /images/optimized/PXL_20231028_171531295-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy outdoor enclosure's cozy perch</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_171614069.jpg" target="_blank" title="A wooden catio structure with mesh enclosure allows indoor cats to safely explore the outdoors from the comfort of their home.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_171614069-300w.webp 300w, /images/optimized/PXL_20231028_171614069-600w.webp 600w, /images/optimized/PXL_20231028_171614069-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy their custom outdoor catio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_171614069-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_171614069-300w.jpeg 300w, /images/optimized/PXL_20231028_171614069-600w.jpeg 600w, /images/optimized/PXL_20231028_171614069-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy their custom outdoor catio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_204747312.jpg" target="_blank" title="A curious cat explores the wooden platforms and shelves of this newly constructed catio, allowing it to safely experience the outdoors.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_204747312-300w.webp 300w, /images/optimized/PXL_20231028_204747312-600w.webp 600w, /images/optimized/PXL_20231028_204747312-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy safe outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_204747312-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_204747312-300w.jpeg 300w, /images/optimized/PXL_20231028_204747312-600w.jpeg 600w, /images/optimized/PXL_20231028_204747312-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy safe outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_204801669.jpg" target="_blank" title="A white and black cat sits on a wooden platform, peering out through the wire mesh enclosure of a newly constructed DIY catio.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_204801669-300w.webp 300w, /images/optimized/PXL_20231028_204801669-600w.webp 600w, /images/optimized/PXL_20231028_204801669-1200w.webp 1200w" sizes="300px"><img alt="Feline enjoys outdoor view from catio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_204801669-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_204801669-300w.jpeg 300w, /images/optimized/PXL_20231028_204801669-600w.jpeg 600w, /images/optimized/PXL_20231028_204801669-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Feline enjoys outdoor view from catio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_204809345.jpg" target="_blank" title="A cat stands on a wooden shelf in a custom-built outdoor enclosure, surrounded by mesh walls and a scenic view.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_204809345-300w.webp 300w, /images/optimized/PXL_20231028_204809345-600w.webp 600w, /images/optimized/PXL_20231028_204809345-1200w.webp 1200w" sizes="300px"><img alt="Cat enjoys new DIY catio enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_204809345-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_204809345-300w.jpeg 300w, /images/optimized/PXL_20231028_204809345-600w.jpeg 600w, /images/optimized/PXL_20231028_204809345-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cat enjoys new DIY catio enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_210433355.jpg" target="_blank" title="An older man sits in a wheelchair, enjoying the outdoor catio he built to allow his cats to safely experience nature.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_210433355-300w.webp 300w, /images/optimized/PXL_20231028_210433355-600w.webp 600w, /images/optimized/PXL_20231028_210433355-1200w.webp 1200w" sizes="300px"><img alt="Senior cat owner relaxes in custom catio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_210433355-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_210433355-300w.jpeg 300w, /images/optimized/PXL_20231028_210433355-600w.jpeg 600w, /images/optimized/PXL_20231028_210433355-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Senior cat owner relaxes in custom catio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_210439784.jpg" target="_blank" title="A cat gazes out through the mesh walls of its newly constructed outdoor catio, exploring its new enriched environment.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_210439784-300w.webp 300w, /images/optimized/PXL_20231028_210439784-600w.webp 600w, /images/optimized/PXL_20231028_210439784-1200w.webp 1200w" sizes="300px"><img alt="Curious cat enjoys outdoor catio view" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_210439784-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_210439784-300w.jpeg 300w, /images/optimized/PXL_20231028_210439784-600w.jpeg 600w, /images/optimized/PXL_20231028_210439784-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Curious cat enjoys outdoor catio view</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_210745540.jpg" target="_blank" title="A cat cautiously traverses the wooden beams of an enclosed patio structure, allowing it to safely enjoy the outdoor environment.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_210745540-300w.webp 300w, /images/optimized/PXL_20231028_210745540-600w.webp 600w, /images/optimized/PXL_20231028_210745540-1200w.webp 1200w" sizes="300px"><img alt="Cat explores custom-built outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_210745540-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_210745540-300w.jpeg 300w, /images/optimized/PXL_20231028_210745540-600w.jpeg 600w, /images/optimized/PXL_20231028_210745540-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cat explores custom-built outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_210748418.jpg" target="_blank" title="A cat relaxes on a wooden perch in the custom-built catio, enjoying the outdoor view through the mesh enclosure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_210748418-300w.webp 300w, /images/optimized/PXL_20231028_210748418-600w.webp 600w, /images/optimized/PXL_20231028_210748418-1200w.webp 1200w" sizes="300px"><img alt="Cozy cat lounging in DIY catio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_210748418-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_210748418-300w.jpeg 300w, /images/optimized/PXL_20231028_210748418-600w.jpeg 600w, /images/optimized/PXL_20231028_210748418-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy cat lounging in DIY catio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_210806274.jpg" target="_blank" title="Man inspecting completed catio enclosure, with a cat perched on the overhead platform.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_210806274-300w.webp 300w, /images/optimized/PXL_20231028_210806274-600w.webp 600w, /images/optimized/PXL_20231028_210806274-1200w.webp 1200w" sizes="300px"><img alt="Catio construction with cat on perch" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_210806274-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_210806274-300w.jpeg 300w, /images/optimized/PXL_20231028_210806274-600w.jpeg 600w, /images/optimized/PXL_20231028_210806274-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Catio construction with cat on perch</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_211603812.jpg" target="_blank" title="Two cats explore the wooden frame and mesh walls of a custom-built outdoor cat patio.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_211603812-300w.webp 300w, /images/optimized/PXL_20231028_211603812-600w.webp 600w, /images/optimized/PXL_20231028_211603812-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy new outdoor enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_211603812-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_211603812-300w.jpeg 300w, /images/optimized/PXL_20231028_211603812-600w.jpeg 600w, /images/optimized/PXL_20231028_211603812-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy new outdoor enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_211610458.jpg" target="_blank" title="Two cats, one black and white and one brown, relax on platforms inside a custom-built catio structure.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_211610458-300w.webp 300w, /images/optimized/PXL_20231028_211610458-600w.webp 600w, /images/optimized/PXL_20231028_211610458-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoy outdoor catio enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_211610458-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_211610458-300w.jpeg 300w, /images/optimized/PXL_20231028_211610458-600w.jpeg 600w, /images/optimized/PXL_20231028_211610458-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoy outdoor catio enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_211625181.jpg" target="_blank" title="Two cats relaxing on elevated platforms in a custom-built catio, surrounded by a wire mesh enclosure and lush green grass.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_211625181-300w.webp 300w, /images/optimized/PXL_20231028_211625181-600w.webp 600w, /images/optimized/PXL_20231028_211625181-1200w.webp 1200w" sizes="300px"><img alt="Cats enjoying outdoor catio enclosure" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_211625181-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_211625181-300w.jpeg 300w, /images/optimized/PXL_20231028_211625181-600w.jpeg 600w, /images/optimized/PXL_20231028_211625181-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats enjoying outdoor catio enclosure</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231028_211626581.jpg" target="_blank" title="Two felines relaxing on wooden platforms within a custom-built, wire-mesh catio enclosure overlooking a grassy backyard.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231028_211626581-300w.webp 300w, /images/optimized/PXL_20231028_211626581-600w.webp 600w, /images/optimized/PXL_20231028_211626581-1200w.webp 1200w" sizes="300px"><img alt="Cats explore new outdoor catio" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231028_211626581-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231028_211626581-300w.jpeg 300w, /images/optimized/PXL_20231028_211626581-600w.jpeg 600w, /images/optimized/PXL_20231028_211626581-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cats explore new outdoor catio</figcaption>
</figure>
<figure class="gallery-item">
  <a href="/images/catio/PXL_20231209_034842721.jpg" target="_blank" title="Illuminated outdoor enclosure for cats, built with modular wooden frame and wire mesh.">
    <picture><source type="image/webp" srcset="/images/optimized/PXL_20231209_034842721-300w.webp 300w, /images/optimized/PXL_20231209_034842721-600w.webp 600w, /images/optimized/PXL_20231209_034842721-1200w.webp 1200w" sizes="300px"><img alt="Cozy catio under night's glow" loading="lazy" decoding="async" class="thumbnail" src="/images/optimized/PXL_20231209_034842721-300w.jpeg" width="1200" height="900" srcset="/images/optimized/PXL_20231209_034842721-300w.jpeg 300w, /images/optimized/PXL_20231209_034842721-600w.jpeg 600w, /images/optimized/PXL_20231209_034842721-1200w.jpeg 1200w" sizes="300px"></picture>
  </a>
  <figcaption>Cozy catio under night's glow</figcaption>
</figure>
</div>
<h2 id="prompts">Prompts Conversation (with <a href="https://claude.ai/" target="_blank"> Claude</a>)</h2>
<p><a href="#">↑ Back to Top</a></p>
<p>This blog post was created with the following prompts:</p>
<div class="question">
ok, time to work your magic.  I would like to add a new blog post (or I would like you to add) for my catio project I did a few years ago.  You can put a placeholder in for the description for now, and pick any random picture from the images/catio folder to use as the preview image.  The actual post should include a thumbnail of each of the images in the catio folder.   For now, lets try a thumbnail size of 300px and see how they look.  I can provide more instructions if you like, but is that enough to get you started?
> Also, I think I want to start something new with each future blog post.  I would like you to add a section at the botom of the post, maybe called "Prompts", and include this prompt in that section.
</div>
<div class="question">
Why are you trying to create a 2023/10 folder?  This post should be in the 2026/02 folder I think.
</div>
<div class="question">
Ouch!  Shouldn't we define a style somewhere for the thumbnails and just use that class on each of the img tags vs putting the width on every singlle img tag?
</div>
<div class="question">
Nice start.  Can we add a link to the top of the post so that I could quickly "jump" to the prompts section at the bottom?
</div>
<div class="question">
error : {pasted terminal error output}
</div>
<div class="question">
I like that. Now, once we are in the Prompts section, we need another "jump to top" link down there to go back to the top of the post.
</div>
<div class="question">
Nice, thanks.   Now I have a bigger question which may require changes that would affect the entire site, but I am ok with that if we can do it.   I am posting a screenshot with a few notes in it.  I am hoping we can reduce some of the whitespace driving the width of the page, so that we would be able to fit three thumbnails across the page.   I want you to keep the responsive look you already have if I make the window smaller (that is working).  But, when it is about this width, I would like to see three thumbnails across.
</div>
<div class="question">
OutFreakingStanding!  I really like that much better, thanks!
</div>
<div class="question">
Now, for my next request, and you may have already done this, but are the images displayed in order starting from oldest to latest?
</div>
<div class="question">
Does the Eleventy build process "optimize" the images in the public folder for the web? I am wondring if people viewing the site on mobile are going to be waiting forever for the full image to download.
</div>
<div class="question">
Yes, please work your magic to optimize my images.
</div>
<p>There were additional prompts after this, but you get the idea.  I believe I am impressed everyday with what AI allows me to do. :-)</p>
<p>I thought I was done, but then I started prompting again in a new Chat to enhance the gallery further:</p>
<div class="question">
I am super impressed with what you have done here.  I could easily leave it this way and be happy.   However, since you are AI and you are great at these kinds of tasks, I am wondering if you can analyze each picture and provide a short caption under each photo that describes it.   If you have longer text to describe a photo, you could add that to the text that displays if I hover over a thumbnail.
Also, can you make each thumbmail a link that opens the full image in a new tab if the user clicks the thumbnail.
</div>
<div class="question">
Looking good, but our first challenge is I think you lost our styling that was giving us 3 images across (on larger screens).   Can you find that and add it back in?
</div>
<p><a href="#">↑ Back to Top</a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Careful what you search for...</title>
      <link href="https://scottbrooks.info/posts/careful-what-you-search-for.../"/>
      <updated>2026-02-22T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/careful-what-you-search-for.../</id>
      <content type="html">
        <![CDATA[
      
    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>My AI Journey in February 2026</title>
      <link href="https://scottbrooks.info/posts/my-ai-journey-in-february-2026/"/>
      <updated>2026-02-24T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/my-ai-journey-in-february-2026/</id>
      <content type="html">
        <![CDATA[
      <h2>Links</h2>
<p><a href="https://skills.sh/" target="_blank">SKILLS</a> - The Open Agent Skills Ecosystem</p>
<p><a href="https://github.com/github/awesome-copilot/tree/main" target="_blank">Awesome GitHub Copilot</a> - community created collection of custom agents, prompts, and instructions to supercharge your GitHub Copilot</p>
<p><a href="https://github.com/anthropics/skills" target="_blank">Anthropic Skills</a> - Anthropic's implementation of skills for Claude</p>
<h2>Videos</h2>
<h3>VS Code Live: Agent Sessions Day</h3>
<div class="youtube-embed">
      <a href="https://www.youtube.com/watch?v=tAezuMSJuFs" target="_blank" rel="noopener">
        <img src="https://img.youtube.com/vi/tAezuMSJuFs/hqdefault.jpg" alt="YouTube video thumbnail" loading="lazy">
        <span class="play-button">▶</span>
      </a>
    </div>
<h3>Keynote: AI-Powered App Development - Steve Sanderson - NDC London 2026</h3>
<div class="youtube-embed">
      <a href="https://www.youtube.com/watch?v=L1w6wBxhpgE" target="_blank" rel="noopener">
        <img src="https://img.youtube.com/vi/L1w6wBxhpgE/hqdefault.jpg" alt="YouTube video thumbnail" loading="lazy">
        <span class="play-button">▶</span>
      </a>
    </div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Help With Zoning</title>
      <link href="https://scottbrooks.info/posts/help-with-zoning/"/>
      <updated>2026-02-26T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/help-with-zoning/</id>
      <content type="html">
        <![CDATA[
      <p>I had the idea that if Claude had access to prior legal cases, it could be helpful to find cases related to zoning and find out who the lawyer was.   This could be a good person to help answer questions about what I hope to do.</p>
<div class="question">
Do you have access to court cases files in Warren County Ohio?
</div>
<img src="/images/zone01.png">
<div class="question">
I am curious about some zoning regulations.   I thought it might be good to talk with an attourney, and it seems like a great way to find a competent attourney in this area would be to look at prior court cases.    Is there an MCP server of something we could possibly connect to get you that type of information?
</div>
<img src="/images/zone02.png">
<img src="/images/zone-mpc.png">
<div class="question">
I think we lost internet there for a moment, can you continue what you were doing?
</div>
<img src="/images/zone03.png">
<div class="question">
Is this something that if I was using Claude Cowork it might do all three of those for me at the same time?
</div>
<img src="/images/zone04.png">
<div class="question">
Can you give me a summary of this conversation in a markdown file that I could give to Cowork to instruct it what we want it to do?
</div>
<h1>Warren County Ohio – Zoning Attorney Research Task</h1>
<h2>Background</h2>
<p>I am a property owner in Warren County, Ohio looking into zoning regulations. I want to find a competent real estate/zoning attorney in the area. The recommended approach is to research prior court cases to identify attorneys with proven experience in Warren County zoning matters.</p>
<h2>Goal</h2>
<p>Research and compile a list of qualified zoning/real estate attorneys who have active case history in Warren County, Ohio. Prioritize attorneys with demonstrated experience in zoning disputes, variances, or land use cases.</p>
<h2>Research Sources</h2>
<p>Please search all three of the following sources and consolidate the results:</p>
<h3>1. Warren County Clerk of Courts</h3>
<ul>
<li>URL: https://www.co.warren.oh.us (navigate to Clerk of Courts / case search)</li>
<li>Warren County likely uses Tyler Technologies' Odyssey case management system</li>
<li>Search for zoning-related cases (keywords: &quot;zoning&quot;, &quot;variance&quot;, &quot;land use&quot;, &quot;R.C. 519&quot;, &quot;R.C. 303&quot;)</li>
<li>Note attorney names appearing on these cases</li>
</ul>
<h3>2. CourtListener / RECAP</h3>
<ul>
<li>URL: https://www.courtlistener.com</li>
<li>Search for Ohio zoning cases, filtering for Warren County or Southern District of Ohio where applicable</li>
<li>Look up attorney profiles tied to relevant cases</li>
<li>API available at https://www.courtlistener.com/api/ if needed</li>
</ul>
<h3>3. Ohio State Bar Association Attorney Search</h3>
<ul>
<li>URL: https://www.ohiobar.org/find-a-lawyer/</li>
<li>Search for attorneys in Warren County, Ohio</li>
<li>Filter by practice area: Real Estate, Zoning, Land Use, or Municipal Law</li>
<li>Cross-reference any names found in court cases above</li>
</ul>
<h2>Deliverable</h2>
<p>Please produce a markdown file (<code>zoning-attorneys-warren-county.md</code>) containing:</p>
<ul>
<li>A table of attorneys found, including:
<ul>
<li>Name</li>
<li>Firm</li>
<li>Contact information (if available)</li>
<li>Number of relevant cases found (if available)</li>
<li>Source(s) where they appeared</li>
</ul>
</li>
<li>A brief notes section on any attorneys who appeared in multiple sources (stronger signal of experience)</li>
<li>Links to relevant case records where possible</li>
</ul>
<h2>Notes</h2>
<ul>
<li>Focus on Warren County, Ohio specifically (county seat: Lebanon, OH)</li>
<li>Zoning matters in Ohio are governed under Ohio Revised Code Chapters 303 (county) and 519 (township)</li>
<li>Springboro, Ohio is within Warren County — cases near Springboro are particularly relevant</li>
</ul>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Using Gemini to troubleshoot engine noise</title>
      <link href="https://scottbrooks.info/posts/using-gemini-to-troubleshoot-engine-noise/"/>
      <updated>2026-02-27T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/using-gemini-to-troubleshoot-engine-noise/</id>
      <content type="html">
        <![CDATA[
      <div class="question">
Can you review this video and determine what is causing the noise in the car?
</div>
<h3>From Claude</h3>
<p><img src="/images/claude-car01.png"><br></p>
<div class="question">
What AI tool can I use to upload a video and have it analyzed?
</div>
<p><img src="/images/claude-car02.png"><br></p>
<h3>From Gemini</h3>
<p><img src="/images/gemini-car-01.png"><br>
<img src="/images/gemini-car-02.png"><br></p>
<div class="question">
Does it help your analysis any if I let you know that it only happens when the engine is cold and first starts. After a few minutes it goes away
</div>
<p><img src="/images/gemini-car-03.png"><br>
<img src="/images/gemini-car-04.png"><br></p>
<br>
<a href="/images/hannah-car-big.png" target="_blank"><img src="/images/hannah-car-big.png" style="width: 100px;"></a>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Website Idea to Working</title>
      <link href="https://scottbrooks.info/posts/website-idea-to-working/"/>
      <updated>2026-03-12T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/website-idea-to-working/</id>
      <content type="html">
        <![CDATA[
      <p>I promise I was paying attention in church — but this idea kept nagging at me anyway.</p>
<p>The sermon that morning was about making decisions <em>before</em> you need to make them. The pastor's point was that when it comes to temptation, you can't wait until you're already in the moment to decide how you'll respond — by then it's too late. You have to resolve it ahead of time.  The point is, I was listening.</p>
<p>Somehow, in true &quot;my brain works differently&quot; fashion, I took that principle and immediately applied it to... building a website. But honestly? There's a real connection there. I'd had this idea rattling around for a while and kept putting it off.  It would be too much work to pull it all together.  Then, I thought maybe this could be something to throw at Claude (AI).</p>
<p>I got home, opened my laptop, and got to work around 1:30 in the afternoon.</p>
<h2>So what is it?</h2>
<p>The site is called <strong><a href="https://votm.us">Verse of the Month</a></strong> and the concept is elegantly simple. Every day, the site takes today's date and uses it to pull a unique cross-section of scripture from across the entire Bible. The current month becomes the chapter number. The current day of the month becomes the verse number. That's it.</p>
<p>So today, March 12th? The site finds every Chapter 3, Verse 12 across all 66 books of the Bible and shows them to you — side by side, ready to explore.</p>
<img src="/images/votm-us-05.png">
<p>What makes it interesting is that the number of verses you see changes every single day. On January 1st (1:1), every book of the Bible has a Chapter 1, Verse 1, so you get all 66. But on a date like November 23rd (11:23), only books with at least 11 chapters <em>and</em> a verse 23 in that chapter will show up — a much smaller, more specific slice of scripture you'd probably never read together otherwise.</p>
<img src="/images/votm-us-01.png">
<h2>It's more than just reading — it's voting</h2>
<p>Once you've browsed the day's verses, you pick your favorite. Your vote gets saved, and you can see how your choice stacks up against everyone else on the leaderboard. It's a small thing, but it adds a fun layer of community to what might otherwise be a solitary habit.</p>
<img src="/images/votm-us-06.png">
<p>The site supports multiple Bible translations and is already available in English, Spanish, and Filipino (Tagalog), with more languages planned. And it's completely free — no data collection, no selling your information. The daily email reminder is totally optional.</p>
<h2>Not bad for a partial day's work</h2>
<p>By 10 PM I had a working site. Now, I'll be honest — I didn't write much of the actual code myself. I built this almost entirely by giving instructions to Microsoft Copilot in VS Code and letting it do the heavy lifting. But don't let that undersell what went into it. Knowing what to ask for, how to ask for it, and being able to recognize when something isn't right — that's its own skill. The site is pulling from Bible APIs to fetch verses, posting back to an API to save votes, filtering by Bible category, and serving users in multiple languages. That's what came out of me and an AI having a conversation about scripture.</p>
<p>Go check it out at <a href="https://votm.us" target="_blank">Verse Of The Month (votm.us)</a><strong><a href=""></a></strong> and cast your vote for today. I'm curious which verse resonates most with you.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Eyes of a Soldier</title>
      <link href="https://scottbrooks.info/posts/eyes-of-a-soldier/"/>
      <updated>2026-03-17T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/eyes-of-a-soldier/</id>
      <content type="html">
        <![CDATA[
      <p>My daughter <a href="/about/#emily" target="_blank">Emily Rose</a> wrote this poem in 2008. She would have been either 16 or 17. I often read it around Veterans Day or Memorial Day. However, last night, a very dear friend showed me some software that can take a poem like this and generate a song from it. I am blown away!  Thanks Larry Collins</p>
<div style="position: relative; width: 100%; max-width: 500px; margin: 2em auto; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
 <div style="position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.7); padding: 1em;">
    <audio controls style="width: 100%;">
      <source src="/images/eyes-of-a-soldier.mp3" type="audio/mpeg">
      Your browser does not support the audio element.
    </audio>
  </div>
  <img src="/images/EyesOfaSoldier.jpeg" alt="Eyes of a Soldier" style="width: 100%; display: block;">
 </div>
<h2>Oliver's request<br></h2>
<p><img src="/images/oliver-crowder.png" style="width: 500px; max-width: 500px;" /><br></p>
<h3>Female version</h3>
<div style="position: relative; width: 100%; max-width: 500px; margin: 2em auto; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
 <div style="position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.7); padding: 1em;">
    <audio controls style="width: 100%;">
      <source src="/images/eyes-of-a-soldier-crowder.mp3" type="audio/mpeg">
      Your browser does not support the audio element.
    </audio>
  </div>
  <img src="/images/EyesOfaSoldier.jpeg" alt="Eyes of a Soldier" style="width: 100%; display: block;">
 </div>
<h3>Male version</h3>
<div style="position: relative; width: 100%; max-width: 500px; margin: 2em auto; border-radius: 8px; overflow: hidden; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
 <div style="position: absolute; bottom: 0; left: 0; right: 0; background: rgba(0,0,0,0.7); padding: 1em;">
    <audio controls style="width: 100%;">
      <source src="/images/male-eyes-of-a-soldier-crowder.mp3" type="audio/mpeg">
      Your browser does not support the audio element.
    </audio>
  </div>
  <img src="/images/EyesOfaSoldier.jpeg" alt="Eyes of a Soldier" style="width: 100%; display: block;">
 </div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>That&#39;s my King</title>
      <link href="https://scottbrooks.info/posts/thats-my-king/"/>
      <updated>2026-04-03T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/thats-my-king/</id>
      <content type="html">
        <![CDATA[
      <p>Here is another one from S. M. Lockridge.<br></p>
<div class="youtube-embed">
      <a href="https://www.youtube.com/watch?v=yzqTFNfeDnE" target="_blank" rel="noopener">
        <img src="https://img.youtube.com/vi/yzqTFNfeDnE/hqdefault.jpg" alt="YouTube video thumbnail" loading="lazy">
        <span class="play-button">▶</span>
      </a>
    </div>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>Big Fire</title>
      <link href="https://scottbrooks.info/posts/big-fire/"/>
      <updated>2026-04-20T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/big-fire/</id>
      <content type="html">
        <![CDATA[
      <p>Click the picture above to see the 30 second video Oliver took of the &quot;event&quot;.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>iTerm2 Cheatsheet</title>
      <link href="https://scottbrooks.info/posts/iterm2-cheatsheet/"/>
      <updated>2026-05-02T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/iterm2-cheatsheet/</id>
      <content type="html">
        <![CDATA[
      <h3>Here was my first prompt</h3>
<p><img src="/images/260502-prompt01.png"><br></p>
<h3>Here was Claude's first attempt</h3>
<p><img src="/images/260502_firstattempt.png"><br></p>
<p>Not bad, but I didn't love losing the second part of the page.<br></p>
<h3>My second prompt.</h3>
<p><img src="/images/260502-prompt02.png"><br></p>
<h3>Claude's second attempt (done)</h3>
<p>That is it.  Now, I have this handy cheatsheet to help me.  (Click the image below to download a copy)
<a href="/images/iTerm2_Cheatsheet.pdf" target="_blank"><img src="/images/260502_secondattempt.png"><br></a></p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>AI Gave Us Two Different Answers — And Both Were &quot;Right&quot;</title>
      <link href="https://scottbrooks.info/posts/ai-questions/"/>
      <updated>2026-05-20T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/ai-questions/</id>
      <content type="html">
        <![CDATA[
      <p>Our team recently had one of those debates that every development shop eventually runs into: where should a particular piece of logic live?</p>
<p>The scenario was straightforward. We have a web application with a frontend built in Angular and a backend API. There are places in the app where we need to show or hide certain fields — maybe columns in a table, maybe inputs on an edit form. The question was simple: should the frontend handle that, or should the backend?</p>
<p>Two developers had two different opinions, which isn't unusual. So someone decided to ask AI for a tiebreaker.</p>
<p>Here's where it gets interesting.</p>
<h2>Round One: The Narrow Question</h2>
<p>The first question was framed pretty tightly. It essentially asked: &quot;Is it okay to pass a configuration object to an Angular component that tells it which fields to show or hide?&quot; No mention of <em>why</em> a field might be hidden. No mention of what drives that decision. Just: can we pass display instructions to the frontend and let it handle the rendering?</p>
<p>AI's answer? <em>Absolutely. That's a perfectly reasonable pattern. Pass a config object to your component and let it toggle field visibility.</em></p>
<p>Fair enough. That <em>is</em> a valid pattern, and if that's all you're doing, the frontend is a fine place for it.</p>
<h2>Round Two: The Full Picture</h2>
<p>Then someone added context. They took the original question — word for word — and appended a bit more detail. Specifically, they explained that the decision about whether a field should be visible or required isn't arbitrary. It depends on things like user permissions, role-based access, and the current state of the data being viewed. Think business rules, not just layout preferences.</p>
<p>Same AI. Same original prompt. Just a few extra sentences of context.</p>
<p>This time the answer was completely different: <em>That logic belongs in your API. Let the backend evaluate the rules and pass the results down to the frontend. The Angular side should just render what it's told.</em></p>
<h2>The Takeaway</h2>
<p>Neither answer was wrong. Both were perfectly valid responses — <em>to the question that was asked.</em> The AI wasn't inconsistent. It simply answered what was in front of it.</p>
<p>And that's the real lesson here. AI doesn't know what you didn't tell it. It can't infer the business rules hiding behind your question. It responds to what you give it, and if you give it a narrow, implementation-focused question, you'll get a narrow, implementation-focused answer. Add the <em>why</em> behind the question — the constraints, the business logic, the broader architecture — and you'll get something much more useful.</p>
<p>This isn't a flaw in AI. It's a mirror. The quality of the answer is directly tied to the quality of the question.</p>
<p>So the next time you're asking AI to weigh in on a technical decision, don't just ask <em>how</em> to do something. Tell it <em>why</em> you need to do it, <em>what drives</em> the decision, and <em>what else</em> is in play. You might be surprised how much the answer changes — and how much better it gets.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>How do nurses share best practices?</title>
      <link href="https://scottbrooks.info/posts/how-do-nurses-share-best-practices/"/>
      <updated>2026-06-12T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/how-do-nurses-share-best-practices/</id>
      <content type="html">
        <![CDATA[
      <div class="question">
What do nurses call their internal training they do to make sure all nurses are aware of current best practices?   The name "in service" is what I keep thinking.
</div>
<p>Yes, you're right — the standard term is <strong>in-service training</strong> (or just <strong>in-services</strong>). Nurses and other healthcare professionals use this to refer to short, focused training sessions conducted on-site, typically covering updated protocols, new equipment, compliance topics (like infection control or HIPAA), or refreshers on best practices.</p>
<p>Related terms you might also hear:</p>
<ul>
<li><strong>Continuing education (CE/CEU)</strong> — more formal, often required for license renewal</li>
<li><strong>Competency training</strong> — skills validation, often annual (e.g., BLS recertification)</li>
<li><strong>Staff development</strong> — broader umbrella term</li>
</ul>
<p>But &quot;in-service&quot; is the everyday, colloquial term nurses themselves use most often.</p>
<div class="question">
I am trying to tie that to software development.   One future project is to start to move developers from using Auzre DevOps for source code to GitHub.   There are differences that will need to be communicated.   My thought was to "require" each developer to attend an "in-service" session before they are granted access to GitHub.   Things like how we name things in GitHub are going to be critical, so making sure people are "told" how things need to be done is important (along with documented standards and other things).   For us, this is important, but it is not life and death.   For nurses, I would expect that some of their training may actually be life or death.   With that being the case, how do they make their approach work and be sure it worked?    What similarities are there that we might apply some of the same principles to what we want to do?
</div>
<p>Great question — nursing in-service training has been refined over decades precisely because the stakes are so high. Here are the key principles they use and how they map to your situation:</p>
<h2>How nursing makes in-services stick</h2>
<p><strong>1. Competency validation, not just attendance</strong>
Nurses don't just sit through a session — they demonstrate competency afterward. A &quot;return demonstration&quot; or skills check confirms they can actually do the thing, not just that they heard about it.</p>
<p><strong>Your equivalent:</strong> Don't just present the GitHub naming conventions and branching strategy. Have each developer complete a hands-on exercise — create a repo, name a branch, open a PR using your standards — before they get production access. Attendance is not comprehension.</p>
<p><strong>2. Closed-loop accountability</strong>
Healthcare tracks who completed training and ties it to privileges. You literally cannot perform certain procedures until your training record is current. No exceptions, no &quot;I'll catch up later.&quot;</p>
<p><strong>Your equivalent:</strong> You're already thinking this way — no GitHub access until the in-service is complete. Stick to it. The moment you make exceptions, the standard collapses.</p>
<p><strong>3. Standardized, written protocols</strong>
Nursing doesn't rely on tribal knowledge. There are written policies that the in-service references. The training teaches the protocol; the protocol outlives the training.</p>
<p><strong>Your equivalent:</strong> Your naming conventions, branching strategy, PR process, etc. should exist as a living document (a repo wiki, a CLAUDE.md, a contributing guide) that the in-service walks through. When someone forgets six months later, they have a reference — not just a memory of a meeting.</p>
<p><strong>4. Small, focused sessions</strong>
In-services are typically 30-60 minutes on one topic. They don't try to boil the ocean. Cognitive overload kills retention.</p>
<p><strong>Your equivalent:</strong> Resist the urge to cover everything about GitHub in one session. Cover what they need on day one: naming, branching model, how to open a PR, where to find help. Advanced topics (Actions, code owners, branch protection) can come later.</p>
<p><strong>5. Teach-back method</strong>
Nurses are often asked to explain what they just learned back to the instructor. This is one of the most effective knowledge-verification techniques in healthcare education — if you can teach it, you understood it.</p>
<p><strong>Your equivalent:</strong> During your session, have developers walk you through the process verbally or on screen. &quot;Show me how you'd name this repo for the payments team's new microservice.&quot; Awkward silence tells you exactly where the gaps are.</p>
<p><strong>6. Annual recurrence and updates</strong>
In-services aren't one-and-done. Protocols change, people forget, new staff arrive. Healthcare re-certifies regularly.</p>
<p><strong>Your equivalent:</strong> Plan a lightweight refresher when your GitHub standards evolve — new naming conventions, new branch protection rules, etc. Doesn't need to be formal, but &quot;we updated the standards, here's what changed&quot; keeps drift from creeping in.</p>
<h2>The core transferable insight</h2>
<p>The nursing model works because it separates <strong>three things most organizations conflate:</strong></p>
<table>
<thead>
<tr>
<th>Nursing principle</th>
<th>Your migration</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Inform</strong> — here's the new protocol</td>
<td>Present your GitHub standards</td>
</tr>
<tr>
<td><strong>Verify</strong> — prove you can do it</td>
<td>Hands-on exercise before access</td>
</tr>
<tr>
<td><strong>Sustain</strong> — written reference + periodic refresh</td>
<td>Living documentation + check-ins</td>
</tr>
</tbody>
</table>
<p>Most dev teams only do the first one (a presentation or a wiki page) and wonder why nobody follows the standards. Nursing assumes that informing alone is insufficient — and they're right, even when the stakes are lower than life and death.</p>
<p>The fact that your stakes are lower actually makes this easier to implement — you don't need formal testing or regulatory compliance. You just need the discipline to say: &quot;Complete the exercise, get access. Here's the doc for when you forget. We'll revisit when things change.&quot;</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>The Longest Month</title>
      <link href="https://scottbrooks.info/posts/the-longest-month/"/>
      <updated>2026-07-31T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/the-longest-month/</id>
      <content type="html">
        <![CDATA[
      <a href="/images/260731_TammyCountdown.jpeg" target="_blank">
  <img src="/images/260731_TammyCountdown.jpeg" alt="Tammy Countdown" style="max-width: 800px;">
</a>
<p>On my whiteboard I drew a calendar to count down the days. Each day has a red number counting down — 31, 30, 29 — and every morning one of those numbers gets a little smaller. Today it reads 21.</p>
<p>Tammy is in the Philippines.  She finally gets to hug our granddaughter Lily for the first time in more than a year.  And, she gets to help with Leah, our second granddaughter.  I'm here in Ohio watching the red numbers count down.</p>
<p>Thirty-one days. A full month apart.</p>
<p>It occurred to me that we haven't been apart this long in over forty years. The last time was back when I was in the Army, deployed to Germany.  Since then, through all the years and all the seasons of a life built together, a month has never come between us.</p>
<p>So this one feels long. The house is quieter. I don't know how many times I just expected her to walk through the door, only to realize that would be pretty difficult right now.</p>
<p>But I know why she's there, and it's a good reason — the best kind. A newborn to hold, and a toddler that is a ball of energy.  Some absences are worth the ache.</p>
<p>Here is a recent picture of Tammy holding Leah:</p>
<a href="/images/260731-Tammy-Leah.jpeg" target="_blank">
  <img src="/images/260731-Tammy-Leah.jpeg" alt="Tammy with Leah" style="max-width: 800px;">
</a>
<p>Twenty-one to go. And then the board comes down, the numbers go back to zero, and this longest month becomes just another story we tell.</p>

    ]]>
      </content>
    </entry>
  
    
    <entry>
      <title>When Is It OK to Lie to Your Spouse?</title>
      <link href="https://scottbrooks.info/posts/when-is-it-ok-to-lie-to-your-spouse/"/>
      <updated>2026-08-01T00:00:00Z</updated>
      <id>https://scottbrooks.info/posts/when-is-it-ok-to-lie-to-your-spouse/</id>
      <content type="html">
        <![CDATA[
      <p>Here's the thing that got me thinking about all this in the first place. Tammy is in the Philippines right now, and while she's away I've been trying to knock out a few projects around the house before she gets home. I'm not planning to tell her about any of them — the idea is that she'll walk in the door, notice them, and be pleasantly surprised. That's not lying.  That's just a husband with a honey-do list and a little time on his hands.</p>
<p>But here's where it gets interesting. It's only <em>not</em> lying right up until the moment she asks me a direct question. If she calls and says, &quot;What are you up to over there?&quot; — well, now I've got a decision to make. And that got me thinking about the whole subject, and about the rules Tammy and I set for ourselves a long time ago.</p>
<p>When Tammy and I first got married, we settled a question most couples never bother to ask out loud: when, if ever, is it okay to lie to each other?</p>
<p>Our answer was short. The only lies allowed were the ones that involved Christmas. Over the years we've been willing to stretch that to cover gifts in general, but the idea is the same.  Outside of surprises meant to make each other smile, we agreed we'd be one hundred percent honest with each other.</p>
<p>Forty-four-plus years later, that little rule has held up better than most of the furniture we've owned. Honesty turned out to be the easy part once we'd decided it wasn't optional.</p>
<p><strong>We took the word &quot;divorce&quot; off the table.</strong></p>
<p>Early on, we decided we simply wouldn't throw that word around. Not in arguments, not as a threat, not even as a &quot;someday maybe&quot; hanging in the air. We weren't going to reach for it when we were angry and wanted to wound, and we weren't going to leave it sitting there as an option we might one day pick up. We were in this for the long haul, and you argue very differently when both people already know the ending is settled. The fight becomes about solving the problem, not about who might walk out the door.</p>
<p>For us, that wasn't just a practical arrangement — it came out of what we believed a marriage is. We didn't sign up for a contract we could cancel when the terms got inconvenient. We entered into a covenant, the kind God takes seriously and keeps even when we don't deserve it. Taking &quot;divorce&quot; off the table was really just our way of saying we meant to keep our word the way He keeps His. When you understand your marriage as a promise made before God and not just to each other, walking away stops being an exit you're looking for and starts being a door you already decided to leave shut.</p>
<p><strong>And then there's the one that only worked because we were broke.</strong></p>
<p>When we got married, we didn't have two nickels to rub together. As it turns out, that was a gift in disguise. In those early arguments — and there were a few — neither of us dared throw anything, because anything worth throwing was something we couldn't afford to replace. Break a plate in a moment of temper and you've just lost a plate you needed for supper. So our fights stayed a little quieter, a little less violent, and we got to keep all our dishes.</p>
<p>I wouldn't recommend poverty as a marriage strategy. But looking back, being too broke to break things taught us something we carried long past the point when we could afford new plates: some restraint is just wisdom you stumbled into early.</p>
<p>So when is it okay to lie to your spouse? Only about the presents. Everything else, tell the truth, keep the ugly words out of the argument, and try not to throw the good china.</p>
<p>Forty-four years in, I can tell you it works.</p>
<p>PS.  This post was created in a conversation with Claude, an AI assistant.  I gave it my initial idea and suggested a few tweaks along the way.  When it gave me the output, I read each word and made some tweaks, but I think it did a great job of helping to convey what I wanted to say.</p>

    ]]>
      </content>
    </entry>
  
</feed>