DEV Community

Cover image for Part 2 - Using Claude, Claude-Dev and Aider to Build a Ticketing System
Joe Giglio, Chief Remote Officer
Joe Giglio, Chief Remote Officer

Posted on • Updated on

Part 2 - Using Claude, Claude-Dev and Aider to Build a Ticketing System

LINK TO PART 1

LINK TO PART 3


The project has been open sourced if you would like to play along at home: https://github.com/joegiglio/helpdesk-claude-dev-ai

PART 2-
I wanted Claude to work on some simple integrations with popular, third party products. But first, I needed to clean up the UI a bit…

[PROMPT]
I don’t like how the “delete icon” looks in the ticket list view. It is just a small red box. Can you verify it looks as intended?
[/PROMPT]

Image

After some back and forth, it was determined that the CSS file from Font Awesome was not loading correctly. Claude made the necessary changes and it was addressed. This did cost me another 20 cents and API credits though. Again, you have to pay for Claude to clean up after itself.

I added another $5 and wanted to switch gears towards Integrations.

[PROMPT]
I would like to make some changes to the Integrations page. 1. Add a fourth column for "Webhooks", with a placeholder image and short description text. 2. Make sure the height of all of the cards is equal. Right now, the Salesforce card is shorter than the others.
[/PROMPT]

Claude fixed it and came back with:

Image

[PROMPT]
This looked better but the text in the cards are not aligned.
[/PROMPT]

I found Claude littering HTML files with inline styles like an animal. I asked it to create a styles.css file and move all CSS into it. I was surprised this was not done earlier in the project but it was quickly addressed.

I also did not like the image used for the Webhooks column since it was intended to represent a WAV file. Odd choice…

Image changes took a couple of tries to get right and I am paying for each request. I don’t want to come across as not being impressed by the power of this technology at my fingertips, so please don’t take it that way… But each request costs money and the daily API requests are limited so I hate having to waste them on follow-up clean up requests, especially on items that are seemingly “simple”.

Final result:

Image

Now that we have a decent looking UI, I wanted to try to build simple integrations into a couple of these services.

It has been a while since I played with Slack Apps. From what I remember, the easiest way to get started was with a webhook. Slack assigns you a URL, you post JSON data to it and the text appears in a chat room. I did not want to waste my Claude credits yet so I spent some time refamiliarizing myself with Slack’s Admin and working with Gemini’s AI in a browser tab.

Turns out posting to a Webhook is still an option and you can find it among all the other bloated Slack app settings. Slack is looking more and more like a Salesforce product; death by 1,000 settings.

Now that I knew how to build the integration, I asked Claude to do the heavy lifting so I could critique his work…

[PROMPT]
I want to begin work on the Integrations feature of this application and want to start with Slack.

Whenever a new ticket is created in this application, I want to send it to a Slack chat room. I am not sure how to do this and need your guidance step-by-step. These steps will include configuration in Slack’s Admin. Let’s start there and then we will create the necessary code in our ticketing system to send data to Slack.
[/PROMPT]

Claude’s answer regarding the necessary set up in Slack’s Admin was correct:

Certainly! I'll guide you through the process of integrating Slack with your ticketing system to send notifications about new tickets. We'll start with the Slack configuration and then move on to implementing the necessary code in your application. Let's break this down into steps:
Step 1: Slack Configuration
To set up the Slack integration, we need to create a Slack app and configure it to receive incoming webhooks. Here's how to do that:
Go to the Slack API website (https://api.slack.com/apps) and sign in with your Slack account.
Click on "Create New App" and choose "From scratch".
Give your app a name (e.g., "Ticketing System") and select the workspace where you want to install it.
Once the app is created, go to "Incoming Webhooks" in the left sidebar and turn on "Activate Incoming Webhooks".
Scroll down and click on "Add New Webhook to Workspace".
Choose the channel where you want the notifications to appear and click "Allow".

You'll be given a Webhook URL. Keep this URL safe and secret, as we'll need it later in our code.
Step 2: Prepare the Application
Now that we have the Slack webhook URL, let's prepare our application to send notifications. We'll need to:
Install the requests library to send HTTP requests to the Slack webhook.
Store the webhook URL securely.
Modify the ticket creation process to send a notification to Slack.
Let's start by installing the requests library

I allowed Claude to proceed with making the necessary code changes to support this feature.

The code changes looked correct at first glance but I did not like that the WEBHOOK_URL variable was added to the app.py file. I thought it was more secure to add it to a separate file and remove that file from the code repo through .gitignore. Claude agreed, apologized for the oversight and cleaned up the code.

Attempting to start the server failed because it was trying to import the requests library which was never installed. It turns out that Claude was not aware that the project was using a virtual environment.

I prompted Claude to use the virtual environment named “.venv” but made a typo and told it to use “.env” instead which added more confusion. After requesting access to the top level of the project directory, it discovered the virtual environment at “.venv” and was able to install requests which cleaned up the problem.

I verified that creating a new ticket automatically posted it to the Slack room:

Image

However, I would like the payload to also have a link to the ticket so whoever is using Slack can click the link to view the ticket in my system. This one is tricky because there is no mechanism yet to view an individual ticket other than the ticket edit screen. I wonder where this will take me…

[PROMPT]
This is working but I would like to change the message payload in the send_slack_notification function. I want to add a link to the new ticket so the viewer of the Slack message can click the link and view the ticket in this system.
[/PROMPT]

Claude indeed suggested linking to the edit_ticket URL…

JIRA 1

Claude also took the liberty of improving the UI a bit, adding a View Ticket button. Nice job!

JIRA 2

I noticed that the sort order for the All Tickets page was not specified so tickets were displayed as “oldest first”. I prefer “newest first” and asked Claude to fix this.

[PROMPT]
In the tickets function, please change the sort order to "newest first".
[/PROMPT]

The query to retrieve all tickets was correctly updated to

tickets = Ticket.query.filter((Ticket.deleted == False) | (Ticket.deleted == None)).order_by(Ticket.created_at.desc()).all()

But how far could I push this? I asked Claude to update the UI to offer a sort dropdown:

[PROMPT]
On the Tickets page, tickets are now being sorted by newest first. This works OK but I would like to add a sort feature to this page. Please add a dropdown list so the tickets can be sorted by Newest First, Oldest First, Assigned To, Priority, Status and Last Updated. Do the sorting client side to avoid having to reload the page. Retain the current default.
[/PROMPT]

Claude gave me a preview of the changes and it looked like some very complex Javascript that would be difficult to maintain. I fired back with:

[PROMPT]
This looks like it will work but also looks like complex code. Is there a way to achieve this functionality with built in features from Bootstrap or jQuery instead?
[/PROMPT]

Claude agreed:

You're absolutely right, and I apologize for suggesting an overly complex solution. Using Bootstrap and jQuery can indeed simplify this process and make the code more maintainable. Let's revise our approach using Bootstrap's built-in sorting functionality with the DataTables plugin, which works well with jQuery and Bootstrap.
First, we need to include the necessary CSS and JavaScript files for DataTables. We'll add these to the base.html template.

Its next iteration added some Bootstrap / jQuery plugins which added striping to the table but the sort dropdown was not visible.

[PROMPT]
I do not see any dropdown to sort the tickets. Please double check?
[/PROMPT]

Claude replies that it did not add a dropdown but instead added functionality to allow sorting by clicking the column heading which is a feature of Bootstrap’s Datatable. That is not what I asked for but it sounded like a potentially better solution. But it was not working!

[PROMPT]
If your intention was to allow sorting by clicking the column headers, I would be OK with that but that is not working either. Please add that functionality instead of the dropdown I originally requested.
[/PROMPT]

Claude thanked me for my flexibility, claimed to fix a few things and gave a nice summary of changes… but it was still not working!

[PROMPT]
I like these proposed changes but I am still not seeing them in the UI. There is no mechanism for sorting or searching. Please double check?
[/PROMPT]

In the middle of debugging this, I got hit with the dreaded:

429 {"type":"error","error":{"type":"rate_limit_error","message":"Number of request tokens has exceeded your per-minute rate limit (https://docs.anthropic.com/en/api/rate-limits); see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."}}

Continuing on after a few minutes… Claude added some console.log statements and asked me to check the server console for these messages. In doing so, I noticed a script error which I reported back.

[PROMPT]
Viewing the console, I see "Script in tickets.html is running", but this is followed by an exception: tickets:160 Uncaught ReferenceError: $ is not defined at tickets:160:1
[/PROMPT]

Claude moved some script tags to the top of the base.html file and everything was working now with a much improved UI!

Tickets

However, it left the debug statements in the console.

[PROMPT]
This is looking much better but you left the console.log debug statements. Please remove them.
[/PROMPT]

Claude apologized and removed the debug statements:

I apologize for overlooking the debug statements. You're absolutely right, and we should remove them for a clean production-ready code. Let's update the tickets.html file to remove the console.log statements.

I did not like the date and time format being used for the ticket list so I asked Claude to make these more human friendly. This seemed to break with the new Datatable component.

Tickets

[PROMPT]
When viewing the ticket list, the date/time format used for the Created At and Updated At columns are not user friendly. Please use the format Month/Day/Year Hour:Minute AM/PM. We will also need to handle timezone considerations so I need you to suggest a fix for that too.
[/PROMPT]

Claude made the necessary changes to the ticket view, added the pytz library and is now displaying tickets in the Pacific time zone. Timezone should be relative to the viewer so we will revisit this when we get to the User Settings page, one of these days.

Just some more cleanup to the Tickets page before we go back to Integrations!

[PROMPT]
In the ticket listing, please update the search box to only search the "Title" column. Right now, all fields are being searched.
[/PROMPT]

Claude’s response:

The ticket list will now display dates in the format "Month/Day/Year Hour:Minute AM/PM" in the US/Pacific timezone, and the search box will only search the "Title" column of the tickets.
These changes provide a more user-friendly date/time format, address timezone considerations, and implement the requested search functionality improvement.

Nice work, Claude. Final UI from this segment:

Tickets

Now let’s revisit Integrations. I had to put another $5 in the tip jar but it is well worth the small investment. I am learning a lot and the project is progressing.

[PROMPT]
I would like to do some more work on the Integrations part of this project. I think it would make sense for each Integration to have its own template page with its own settings that are stored in a new database table. For example, the Slack page should have a checkbox toggle for the user to decide whether to send new ticket data to Slack. It should also have a text field for the Slack Webhook URL, which should be used instead of the hardcoded URL that is now found in the config.py file.
The other Integrations - Salesforce, JIRA and Webhook should also have their own pages and settings, all stored in the database, which we will be building out once changes to the Slack page are satisfactory.
Each of these pages should inherit the lefthand navigation bar which should always remain visible during navigation.
[/PROMPT]

Claude made the necessary changes to the files and database and it worked fine. Nice job but it needed some UI tweaks.

[PROMPT]
This setting should be more descriptive, asking if the user would like to send newly created tickets to Slack. Also, enforce logic that if this setting is enabled, the URL field must have a valid URL before saving.
[/PROMPT]

Claude showed its proposed changes but I did not like that it was using plain Javascript. I would rather use jQuery and I asked for the change…

[PROMPT]
It looks like you are using plain Javascript. Please use jQuery for the Javascript portion.
[/PROMPT]

This change was working but I still wanted some more UI tweaks…

[PROMPT]
Those changes to the Slack integration page are working great but I would like some UI tweaks: 1. The spacing in the integrations/slack URL is too tight and needs to be improved. 2. Please use a flash message to show a success message when changes are saved.
[/PROMPT]

Claude showed its proposed changes but I did not like that it was putting the code for the Flash messages on the Integrations page. It should go in the base.html file.

[PROMPT]
That flashed_messages code might be better off in the base.html file so it can be used on any page that inherits it. Please confirm.
[/PROMPT]

Claude agreed…

[PROMPT]
That is working but can you use the type of Flash message that disappears automatically after a few seconds?
[/PROMPT]

Looking good. One more UI tweak and we should be there…

Integrations

[PROMPT]
This is looking good but I would like to add a Slack logo to the integrations/slack page.
[/PROMPT]

Claude wants to make a new “static/images” directory with the file which makes sense to me. This is when I realized that the images being used so far were not stored locally. They were A HREFs pulling images from across the web. I want to use local images instead and requested this change…

[PROMPT]
This makes sense. I see there are images in the integrations.html file that are linked from the web and not local. All of those images should be stored locally in this new static/images directory. The code should be updated to use them. Please also make this change.
[/PROMPT]

Claude made the necessary code changes and ran a cURL command to copy the images locally. Nicely done!

Files

[PROMPT]
Now that this is done, I want to follow the same format for the other integrations on the Integrations page - Salesforce, JIRA and Webhook. Please use the same Configure button which should link to each Integration page in the templates directory. Add the relevant logo and placeholder text since there are no settings for the other integrations yet.
[/PROMPT]

I hit an API limit while Claude was iterating so needed to take a break. Here is the UI from the final iteration:

UI

-

UI

-

UI

Now on to JIRA integration. To keep it simple, I want to do the same as the Slack integration - namely create a new JIRA ticket using an API or webhook. The JIRA Admin and API docs are a maze of settings, so once again I asked Gemini for a head start:

[PROMPT]
You are an expert Python and Flask developer. I am developing an application in Python and Flask and I need to integrate with JIRA. I want to use the JIRA API to create new tickets. How can I do so? Is there a Python JIRA package or a webhook I should be calling? Please let me know what I need to enable in the JIRA Admin and give me some code examples.
[/PROMPT]

It gave me a couple of pointers on where to create an API key but I did not love this answer: “Depending on your JIRA instance (Cloud or Server), you might need to enable specific API permissions for your user or application. Refer to your JIRA administration documentation for details.”

If I was that smart, I wouldn’t need AI. 🙂

It did generate a Python code sample making use of the jira Python package. Let’s see what Claude has to say.

[PROMPT]
I would now like to create an integration into JIRA with this ticketing application. We need to create functionality similar to what was created for the Slack Integration piece, namely sending newly created tickets to JIRA.

How can I do so? Is there a Python JIRA package or a webhook I should be calling? Please let me know what I need to enable in the JIRA Admin and then make the necessary changes to this project.
[/PROMPT]

Claude made a bunch of code and database changes to accommodate my request. At first glance, they looked OK but the UI for the JIRA settings page removed the JIRA logo so I asked for it to be restored.

I also did not like the wording for the toggle, asking the user if he wanted to enable JIRA integration with no further context. I wanted it to match the wording on the Slack integration page:

[PROMPT]
I want the UX to match the one you created for the Slack integration page where the checkbox toggle reads "Send newly created tickets to Slack". Please do the same for the JIRA page.
[/PROMPT]

Claude made the change but each roundtrip costs money. A tweak like this could have easily been done manually but I am trying to see how much I can build with only AI prompts and I was curious to see if it would use existing UIs it already created as a guideline. It did not.

It also did not do a great job of telling me how to enable the API on the JIRA side whereas Gemini spelled out at least some of it.

Claude:

In the JIRA Admin panel:
Create a new project for your helpdesk tickets (note down the project key)
Create an API token for your JIRA account

I asked for clarification on how to create the JIRA API key but hit my daily API limit as it was coming up with an answer:

429 {"type":"error","error":{"type":"rate_limit_error","message":"Number of request tokens has exceeded your daily rate limit (https://docs.anthropic.com/en/api/rate-limits); see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."}}

With some trial and error, I was able to create the API key and learned that the JIRA API URL is just the URL for your instance of JIRA. Easy enough. I put these entries in my system’s admin page and tried to create a ticket. Ticket creation worked but I saw this exception in the console:

response text = {"errorMessages":[],"errors":{"priority":"Field 'priority' cannot be set. It is not on the appropriate screen, or unknown."}}

The code generated by Claude was trying to set the Priority of the ticket in JIRA but that field does not exist in a default JIRA task list. I commented out this line, tried again and it worked! You can also add the field in the JIRA project settings as another possible fix. A more robust JIRA integration would need to have context of the JIRA project, available fields, etc.

I did see a new field was created in the database for “jira_issue_key” but it is always blank. It would be a nice touch to record this and have it visible in the view / edit ticket screen. I will try to clean this up once my daily API limits reset.

On to the next day. I hope Claude is feeling refreshed…

[PROMPT]
During the last task, you created the ability to send new tickets to JIRA. That is working now but it looks like the feature may be incomplete. You created a new database field in the ticket table named “jira_issue_key” but it is always blank. Can you fix this?
[/PROMPT]

Claude added some new code but it still didn’t work…

[PROMPT]
I just tried to create a new ticket in our system but the jira_issue_key field is still always blank in the ticket table.
[/PROMPT]

Claude is very slow today and is returning lots of 500 errors:

500 {"type":"error","error":{"type":"api_error","message":"Internal server error"}}

I went through about a dozen iterations with Claude trying to fix the problem. It added lots of logging and then some very complex database code that was way above my head. It was still not working.

[PROMPT]
Here is the latest log: INFO:main:New ticket created: #20
INFO:main:Slack notification sent for ticket #20
INFO:main:JIRA issue created: KAN-9 for ticket #20
INFO:main:JIRA issue key KAN-9 assigned to ticket #20
INFO:main:Before commit: Ticket #20 JIRA issue key: KAN-9
INFO:main:All changes committed for ticket #20
INFO:main:After commit: Ticket #20 JIRA issue key: KAN-9
ERROR:main:Failed to retrieve ticket #20 from database after commit And I have noticed that the ticket is no longer being created in the database although I am getting a Success Flash message. This is worse than the original problem. Before, I had an empty column but now I have an empty row and a false success message. Data is being lost without warning in the UI!
[/PROMPT]

I am probably going to run out of API requests soon and Claude can’t seem to solve this one and the code is in an ugly place now.

Thankfully I saved a commit as a restore point and if I disable JIRA I am still able to create tickets so the system is not completely crippled. I will put JIRA integration to bed for now and see if Claude can help me with some other features.

Or maybe not. Look who it is:

429 {"type":"error","error":{"type":"rate_limit_error","message":"Number of request tokens has exceeded your daily rate limit (https://docs.anthropic.com/en/api/rate-limits); see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."}}

After looking at Claude’s code and the logs being generated, I vaguely remember having a similar problem in an old project. It would create a database entry and then needed to update the row immediately but Flask could not find the record. The problem was fixed with a db.session.flush() in my old project and I thought I would look like a genius by adding the fix here, but no luck. But I figured I would suggest it to Claude as a clue... Maybe he knows what to do with it.

The next day:

[PROMPT]
Yesterday, when we left off I was able to use this system to create a new ticket. However, if JIRA integration is enabled the jira_issue_key is always blank in the tickets table. You tried a bunch of different code and logging options but were not able to fix it. Having to try so many times has cost me some money with the API so I want to make sure we are going down the right path. In trying to debug it myself, I tried to use db.session.flush() before db.session.commit() but that also had the same problem. The ticket is created, but the jira_issue_key field is always blank. Can you try to fix this again? I can only allow 3 more tries though since this is getting expensive!
[PROMPT]

I hoped that would light a fire under his ass and keep him focused!

I told Claude he only had 3 tries but I gave him 8. The issue is still not fixed. I am stumped and he is stumped but I need to move on. This will likely be throwaway code anyway but issues like this drive me crazy!

I did find that if the table had an entry for the JIRA ticket ID, a button appeared that properly linked to the JIRA ticket which opened in a new window. So close, yet so far…

View JIRA issue

I would like to add a couple of new features: A knowledge base and “customer portal” where customers can submit tickets.

As a multi-channel ticketing system, customers should be able to submit support cases through email, webform, phone, Twitter, Facebook, SMS, etc. Webform via customer portal is the easiest so let’s start there…

[PROMPT]
I would like to create a new feature in the product and we will need to do it in several steps. The feature will allow end users to submit support tickets through a webform in our portal, neither of which exist yet. Please create new templates for customer facing pages: 1. A knowledge base with placeholder graphics and text. 2. This page should link to a webform where a user can submit a support case. The required fields are name, email address, subject and description. When a user submits the webform, it should create a new case in our system.
[/PROMPT]

While reviewing the proposed code, I noticed Claude was creating a local placeholder image. Normally I would be fine with this but other placeholder images throughout the product were done using via.placeholder.com. I asked Claude to re-use that service to keep the code consistent. I know that earlier I wanted Claude to use local images. This time I wanted to see if it would continue using the design pattern it started using earlier. It did not…

Claude was also creating some custom CSS for card sizing and I suggested he use the built in Bootstrap classes and he agreed that would be a better idea. His initial suggestions may work but they are not always best practices or consistent with the rest of the code base… and this is a simple codebase that Claude has created himself. Always double check the code before accepting it!

Here is the proposed UI for the Knowledge Base skeleton:

KB

Even though that looked OK, I took a screenshot of a Knowledge Base UI that I liked and asked if Claude could build a UI similar to it. Computer Vision is supposedly one of Claude’s strengths and I have not used it yet. So I asked…

[PROMPT]
I would like to change the Knowledge Base UI found in the knowledge-base URL to be more like this image. Can you emulate this?
[/PROMPT]

KB

This is what Claude came up with. Simple, clean and minimalistic… Not bad!

Image

Moving on, I asked Claude to create a webform so users can submit new tickets. This worked fine but I wanted to modernize the ticket list UX so it shows new tickets without having to refresh. Off the rails we go!

[PROMPT]
I would like the tickets table to automatically refresh if the system receives a new ticket. Maybe we can use something like a websocket? New tickets should also flash yellow when they appear.
[/PROMPT]

Claude added the usual code for sockets-io, etc. The first attempt did not work - new tickets were created but the tickets Datatable did not update so Claude added some logging. Funny, I have this problem all the time with websockets too. I find it very confusing and it never works without some hair pulling.

Claude:

Thank you for providing the error message. It appears that there's an issue with the WebSocket connection. Let's try to resolve this by making some adjustments to our WebSocket configuration.
First, let's update the app.py file to ensure that CORS (Cross-Origin Resource Sharing) is properly configured for the WebSocket connection

API Request Failed

429 {"type":"error","error":{"type":"rate_limit_error","message":"Number of request tokens has exceeded your daily rate limit (https://docs.anthropic.com/en/api/rate-limits); see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."}}

This API limit is going to kill me.

On to a new day...

We went round and round about a dozen times with websocket errors and logging but Claude can’t seem to fix this one.

The console logging suggest that it should be working fine but when I submit a new ticket, a Javascript alert pops up:

Image

I asked Claude if he had access to view live pages on the web and suggested he review the following URLs for potential fixes:

https://datatables.net/forums/discussion/42814/requested-unknown-parameter-object-object
And http://datatables.net/tn/4

He reviewed the pages, changed the format of the data being passed to the Datatable but still no luck! Similar to the JIRA ticket issue, it looks like Claude is stumped, my API credits are about to run out and I am left with code that is littered with attempted fixes and logging. Again, good thing I committed code of the last good build.

Hey, guess who came to dinner:

API Request Failed

429 {"type":"error","error":{"type":"rate_limit_error","message":"Number of request tokens has exceeded your daily rate limit (https://docs.anthropic.com/en/api/rate-limits); see the response headers for current usage. Please reduce the prompt length or the maximum tokens requested, or try again later. You may also contact sales at https://www.anthropic.com/contact-sales to discuss your options for a rate limit increase."}}

[SIDEBAR]

I asked Gemini if it could build a UI that emulated the same knowledge base UI that I fed to Claude. As a reminder, this way the image I used:

KB

This was Gemini’s interpretation after several iterations. The styling is all off, everything is off center, etc. I am surprised that the results are so poor. I did not spend more than 15 minutes on it since it was not progressing much…

Gemini KB

I am trying to use Gemini to fill in the gaps when Claude hits me with API limits. I guess its Computer Vision is not yet up to snuff though…

[/SIDEBAR]

Painful. To be continued when I regain my strength. 🙂

LINK TO PART 3


About the Author

Joe Giglio is a 25+ year veteran of the software industry. In his career, he has worked for a local dial-up ISP, startups of all sizes and a well established, slow moving enterprise. There were important lessons learned along every stop.

His recent projects include:

Joe’s guiding principles are: Remote first, quality driven, customer champion and lifelong learning. He was an introvert and practiced social distancing long before it was cool!

He currently resides in North Carolina and is always interested in new opportunities, meeting quality people and learning about exciting projects.

Contact Joe Giglio: joe -AT- joegiglio.org | Twitter | LinkedIn

Top comments (0)